Compare commits
213 Commits
l10n_devel
...
junepark67
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d49098a8be | ||
|
|
b4e18c50d3 | ||
|
|
d18482a04a | ||
|
|
f3d9dd777d | ||
|
|
e117c4b9a3 | ||
|
|
95666178e5 | ||
|
|
56403466b9 | ||
|
|
c7344ef548 | ||
|
|
71c4abfce8 | ||
|
|
0613af2240 | ||
|
|
dd832ad6df | ||
|
|
8cf7bc9998 | ||
|
|
c1cf11c04c | ||
|
|
0058c40f46 | ||
|
|
1397389f95 | ||
|
|
6a2d3e1d22 | ||
|
|
46ac704013 | ||
|
|
0fdab2a5c5 | ||
|
|
8f4586bfef | ||
|
|
52d0c9861f | ||
|
|
feace61eb4 | ||
|
|
1f5cc8f283 | ||
|
|
60b8520237 | ||
|
|
6a67c5e9a2 | ||
|
|
bcc241518c | ||
|
|
6dfa8f1556 | ||
|
|
19dde692b2 | ||
|
|
14dc93b5d2 | ||
|
|
547620235e | ||
|
|
b43fd0a54b | ||
|
|
8b782c9416 | ||
|
|
aab4e62e24 | ||
|
|
1713fccfc4 | ||
|
|
83ece72ae1 | ||
|
|
d60bcc49e1 | ||
|
|
bc9c37adda | ||
|
|
2583c7f617 | ||
|
|
fea5229e02 | ||
|
|
68be615057 | ||
|
|
370cafcba0 | ||
|
|
f923c1602e | ||
|
|
50a85be872 | ||
|
|
aae4725a3c | ||
|
|
9d76ee9f19 | ||
|
|
34a101b796 | ||
|
|
49b1fd751c | ||
|
|
4c5bf7bb7d | ||
|
|
2d71631d93 | ||
|
|
fa0d933956 | ||
|
|
b5d6384a07 | ||
|
|
d39644a4c9 | ||
|
|
a2feb34dc1 | ||
|
|
7e5fe64153 | ||
|
|
44175d071c | ||
|
|
bae26de444 | ||
|
|
b78707808d | ||
|
|
d41518581a | ||
|
|
4abbfe6142 | ||
|
|
dae813d80c | ||
|
|
af89b178ad | ||
|
|
8c269207fd | ||
|
|
42ecd38517 | ||
|
|
9f7d4dee49 | ||
|
|
458b8e491e | ||
|
|
495e621e69 | ||
|
|
c986512b5f | ||
|
|
d277754ae5 | ||
|
|
2ef2e2f26b | ||
|
|
23a53034fa | ||
|
|
ce57d72a78 | ||
|
|
502b89d890 | ||
|
|
5f0015fad0 | ||
|
|
c81236957b | ||
|
|
970ab38b27 | ||
|
|
8a5c31b81d | ||
|
|
8508fe79b5 | ||
|
|
3859e98801 | ||
|
|
a759c7be9e | ||
|
|
12fc6cf6e2 | ||
|
|
580db6530e | ||
|
|
9c67c237ee | ||
|
|
357d85a72e | ||
|
|
88ad828ce0 | ||
|
|
a95625a34a | ||
|
|
95e00d81f5 | ||
|
|
c2e386a5c5 | ||
|
|
a76aade4ff | ||
|
|
65c9986103 | ||
|
|
9e2b9b6639 | ||
|
|
cf373634d7 | ||
|
|
b3d5d976b4 | ||
|
|
c3c31995ce | ||
|
|
7e92e17429 | ||
|
|
88ab8fa8d7 | ||
|
|
ebe78932bf | ||
|
|
2e613e6d15 | ||
|
|
35ee92db12 | ||
|
|
04d9f760ad | ||
|
|
4f52743be8 | ||
|
|
32cae7a5b2 | ||
|
|
c2c0e3b790 | ||
|
|
6d36a30787 | ||
|
|
48a86ec6de | ||
|
|
5cff914ff3 | ||
|
|
70ea725ce3 | ||
|
|
78f12e45f9 | ||
|
|
e5061acc20 | ||
|
|
2d7bc51d30 | ||
|
|
9128b67ee8 | ||
|
|
551c004476 | ||
|
|
ed6a8d1379 | ||
|
|
766fb89e0b | ||
|
|
c5b8cb4459 | ||
|
|
0deae92829 | ||
|
|
cc5d2f1813 | ||
|
|
41151d0d49 | ||
|
|
52702264a3 | ||
|
|
6e297e1278 | ||
|
|
e3bb9b425f | ||
|
|
79255be79c | ||
|
|
7c836f5ba1 | ||
|
|
938bcd14ad | ||
|
|
229d79fc05 | ||
|
|
2d3dac2e1d | ||
|
|
e23f5e7894 | ||
|
|
571d27c814 | ||
|
|
dde6bd4fe3 | ||
|
|
6e6dbd9329 | ||
|
|
258268f5ef | ||
|
|
9ae49977fb | ||
|
|
d61c54fa60 | ||
|
|
980699af6f | ||
|
|
cc5c280882 | ||
|
|
090456bba1 | ||
|
|
5354d4eb76 | ||
|
|
b986fae611 | ||
|
|
cfcfc3e928 | ||
|
|
f97548fc3a | ||
|
|
36913b425c | ||
|
|
822ea08d89 | ||
|
|
98dd6f3fe7 | ||
|
|
b3f0dbb155 | ||
|
|
6904d931c3 | ||
|
|
529466a9f7 | ||
|
|
77dc695ba1 | ||
|
|
e17776f651 | ||
|
|
0d2f355a74 | ||
|
|
2ce1576016 | ||
|
|
0f3be3c494 | ||
|
|
8c1ca8503a | ||
|
|
32a59c17f4 | ||
|
|
b4b4ceab0b | ||
|
|
be1f27bb9e | ||
|
|
ed10ddb1cb | ||
|
|
dbdb4b0f32 | ||
|
|
59e537362e | ||
|
|
6d96bf414f | ||
|
|
e7ba778a5f | ||
|
|
933d349cd5 | ||
|
|
3de24dcfce | ||
|
|
3275d16b8b | ||
|
|
5bb4cd1dad | ||
|
|
16b14441fa | ||
|
|
93a6272d30 | ||
|
|
0dc526f778 | ||
|
|
183e185812 | ||
|
|
e02453598c | ||
|
|
24af1b5b5f | ||
|
|
5864c283f6 | ||
|
|
be78fa4b91 | ||
|
|
b3abf69a02 | ||
|
|
c530dc11ae | ||
|
|
d368ddbd11 | ||
|
|
e5c6521a15 | ||
|
|
898a59768e | ||
|
|
a85bc93142 | ||
|
|
c6c1f9faa0 | ||
|
|
0eea19c9cc | ||
|
|
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 |
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
|||||||
* @JoeMatt @lonkelle
|
* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD
|
||||||
|
|||||||
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -2,15 +2,14 @@ name: Bug Report
|
|||||||
description: Report a bug
|
description: Report a bug
|
||||||
title: "[BUG] "
|
title: "[BUG] "
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
assignees:
|
assignees: []
|
||||||
- naturecodevoid
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
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.
|
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.**
|
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,7 +3,7 @@ blank_issues_enabled: false
|
|||||||
|
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Discord
|
- name: Discord
|
||||||
url: https://discord.gg/RgpFBX3Q3k
|
url: https://discord.gg/sidestore-949183273383395328
|
||||||
about: If you need support, please go here first instead of making an issue!
|
about: If you need support, please go here first instead of making an issue!
|
||||||
- name: GitHub Discussions
|
- name: GitHub Discussions
|
||||||
url: https://github.com/SideStore/SideStore/discussions
|
url: https://github.com/SideStore/SideStore/discussions
|
||||||
|
|||||||
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -2,15 +2,14 @@ name: Feature Request
|
|||||||
description: Suggest a feature
|
description: Suggest a feature
|
||||||
title: "[FEATURE REQUEST] "
|
title: "[FEATURE REQUEST] "
|
||||||
labels: ["enhancement"]
|
labels: ["enhancement"]
|
||||||
assignees:
|
assignees: []
|
||||||
- naturecodevoid
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
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.
|
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.**
|
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
3
.github/pull_request_template.md
vendored
@@ -10,6 +10,3 @@
|
|||||||
<!-- Example: -->
|
<!-- Example: -->
|
||||||
- [x] Finish UI changes
|
- [x] Finish UI changes
|
||||||
- [ ] Test
|
- [ ] Test
|
||||||
|
|
||||||
<!-- If your PR doesn't close an issue, you can remove the next line. -->
|
|
||||||
Closes #1234
|
|
||||||
|
|||||||
55
.github/workflows/attach_build_products.yml
vendored
@@ -20,3 +20,58 @@ jobs:
|
|||||||
format: name
|
format: name
|
||||||
addTo: pull
|
addTo: pull
|
||||||
# addTo: pullandissues
|
# addTo: pullandissues
|
||||||
|
nightly-link-comment:
|
||||||
|
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
# This snippet is public-domain, taken from
|
||||||
|
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
|
||||||
|
script: |
|
||||||
|
async function upsertComment(owner, repo, issue_number, purpose, body) {
|
||||||
|
const {data: comments} = await github.rest.issues.listComments(
|
||||||
|
{owner, repo, issue_number});
|
||||||
|
|
||||||
|
const marker = `<!-- bot: ${purpose} -->`;
|
||||||
|
body = marker + "\n" + body;
|
||||||
|
|
||||||
|
const existing = comments.filter((c) => c.body.includes(marker));
|
||||||
|
if (existing.length > 0) {
|
||||||
|
const last = existing[existing.length - 1];
|
||||||
|
core.info(`Updating comment ${last.id}`);
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner, repo,
|
||||||
|
body,
|
||||||
|
comment_id: last.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
core.info(`Creating a comment in issue / PR #${issue_number}`);
|
||||||
|
await github.rest.issues.createComment({issue_number, body, owner, repo});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {owner, repo} = context.repo;
|
||||||
|
const run_id = ${{github.event.workflow_run.id}};
|
||||||
|
|
||||||
|
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
|
||||||
|
if (!pull_requests.length) {
|
||||||
|
return core.error("This workflow doesn't match any pull requests!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifacts = await github.paginate(
|
||||||
|
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
|
||||||
|
if (!artifacts.length) {
|
||||||
|
return core.error(`No artifacts found`);
|
||||||
|
}
|
||||||
|
let body = `Download the artifacts for this pull request (nightly.link):\n`;
|
||||||
|
for (const art of artifacts) {
|
||||||
|
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info("Review thread message body:", body);
|
||||||
|
|
||||||
|
for (const pr of pull_requests) {
|
||||||
|
await upsertComment(owner, repo, pr.number,
|
||||||
|
"nightly-link", body);
|
||||||
|
}
|
||||||
|
|||||||
45
.github/workflows/beta.yml
vendored
@@ -27,11 +27,25 @@ jobs:
|
|||||||
- name: Change version to tag
|
- name: Change version to tag
|
||||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
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
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Cache Build
|
||||||
|
uses: irgaly/xcode-cache@v1
|
||||||
|
with:
|
||||||
|
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||||
|
restore-keys: xcode-cache-deriveddata
|
||||||
|
|
||||||
- name: Build SideStore
|
- name: Build SideStore
|
||||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
@@ -41,22 +55,6 @@ jobs:
|
|||||||
- name: Convert to IPA
|
- name: Convert to IPA
|
||||||
run: make ipa
|
run: make ipa
|
||||||
|
|
||||||
- name: Upload SideStore.ipa Artifact
|
|
||||||
uses: actions/upload-artifact@v3.1.0
|
|
||||||
with:
|
|
||||||
name: SideStore.ipa
|
|
||||||
path: SideStore.ipa
|
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
|
||||||
uses: actions/upload-artifact@v3.1.0
|
|
||||||
with:
|
|
||||||
name: SideStore-dSYM
|
|
||||||
path: ./*.dSYM/
|
|
||||||
|
|
||||||
- name: Get version
|
|
||||||
id: version
|
|
||||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
@@ -88,3 +86,18 @@ jobs:
|
|||||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||||
Commit SHA: `${{ github.sha }}`
|
Commit SHA: `${{ github.sha }}`
|
||||||
Version: `${{ steps.version.outputs.version }}`
|
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/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ DATE=`date -u +'%Y.%m.%d'`
|
|||||||
BUILD_NUM=1
|
BUILD_NUM=1
|
||||||
|
|
||||||
write() {
|
write() {
|
||||||
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM/" -i '' Build.xcconfig
|
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||||
echo "$DATE,$BUILD_NUM" > .nightly-build-num
|
echo "$DATE,$BUILD_NUM" > .nightly-build-num
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
.github/workflows/nightly.yml
vendored
@@ -36,11 +36,24 @@ jobs:
|
|||||||
- name: Increase nightly build number and set as version
|
- name: Increase nightly build number and set as version
|
||||||
run: bash .github/workflows/increase-nightly-build-num.sh
|
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
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: Cache Build
|
||||||
|
uses: irgaly/xcode-cache@v1
|
||||||
|
with:
|
||||||
|
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||||
|
restore-keys: xcode-cache-deriveddata-
|
||||||
|
|
||||||
- name: Build SideStore
|
- name: Build SideStore
|
||||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
@@ -50,22 +63,6 @@ jobs:
|
|||||||
- name: Convert to IPA
|
- name: Convert to IPA
|
||||||
run: make ipa
|
run: make ipa
|
||||||
|
|
||||||
- name: Upload SideStore.ipa Artifact
|
|
||||||
uses: actions/upload-artifact@v3.1.0
|
|
||||||
with:
|
|
||||||
name: SideStore.ipa
|
|
||||||
path: SideStore.ipa
|
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
|
||||||
uses: actions/upload-artifact@v3.1.0
|
|
||||||
with:
|
|
||||||
name: SideStore-dSYM
|
|
||||||
path: ./*.dSYM/
|
|
||||||
|
|
||||||
- name: Get version
|
|
||||||
id: version
|
|
||||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
@@ -96,5 +93,17 @@ jobs:
|
|||||||
Commit SHA: `${{ github.sha }}`
|
Commit SHA: `${{ github.sha }}`
|
||||||
Version: `${{ steps.version.outputs.version }}`
|
Version: `${{ steps.version.outputs.version }}`
|
||||||
|
|
||||||
- name: Reset cache for apps.sidestore.io/nightly
|
- name: Add version to IPA file name
|
||||||
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
|
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/
|
||||||
|
|||||||
26
.github/workflows/pr.yml
vendored
@@ -23,13 +23,28 @@ jobs:
|
|||||||
run: brew install ldid
|
run: brew install ldid
|
||||||
|
|
||||||
- name: Add PR suffix to version
|
- name: Add PR suffix to version
|
||||||
run: sed -e '/MARKETING_VERSION = .*/s/$/-pr.${{ github.event.pull_request.number }}/' -i '' Build.xcconfig
|
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
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: Cache Build
|
||||||
|
uses: irgaly/xcode-cache@v1
|
||||||
|
with:
|
||||||
|
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||||
|
restore-keys: xcode-cache-deriveddata-
|
||||||
|
|
||||||
- name: Build SideStore
|
- name: Build SideStore
|
||||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
@@ -39,14 +54,17 @@ jobs:
|
|||||||
- name: Convert to IPA
|
- name: Convert to IPA
|
||||||
run: make 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
|
- name: Upload SideStore.ipa Artifact
|
||||||
uses: actions/upload-artifact@v3.1.0
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore.ipa
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
path: SideStore.ipa
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
- name: Upload *.dSYM Artifact
|
||||||
uses: actions/upload-artifact@v3.1.0
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore-dSYM
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
path: ./*.dSYM/
|
path: ./*.dSYM/
|
||||||
|
|||||||
44
.github/workflows/stable.yml
vendored
@@ -27,11 +27,24 @@ jobs:
|
|||||||
- name: Change version to tag
|
- name: Change version to tag
|
||||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
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
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: Cache Build
|
||||||
|
uses: irgaly/xcode-cache@v1
|
||||||
|
with:
|
||||||
|
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||||
|
restore-keys: xcode-cache-deriveddata-
|
||||||
|
|
||||||
- name: Build SideStore
|
- name: Build SideStore
|
||||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
@@ -41,22 +54,6 @@ jobs:
|
|||||||
- name: Convert to IPA
|
- name: Convert to IPA
|
||||||
run: make ipa
|
run: make ipa
|
||||||
|
|
||||||
- name: Upload SideStore.ipa Artifact
|
|
||||||
uses: actions/upload-artifact@v3.1.0
|
|
||||||
with:
|
|
||||||
name: SideStore.ipa
|
|
||||||
path: SideStore.ipa
|
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
|
||||||
uses: actions/upload-artifact@v3.1.0
|
|
||||||
with:
|
|
||||||
name: SideStore-dSYM
|
|
||||||
path: ./*.dSYM/
|
|
||||||
|
|
||||||
- name: Get version
|
|
||||||
id: version
|
|
||||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
@@ -85,3 +82,18 @@ jobs:
|
|||||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||||
Commit SHA: `${{ github.sha }}`
|
Commit SHA: `${{ github.sha }}`
|
||||||
Version: `${{ steps.version.outputs.version }}`
|
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/
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -19,7 +19,7 @@ archive.xcarchive
|
|||||||
*.perspectivev3
|
*.perspectivev3
|
||||||
!default.perspectivev3
|
!default.perspectivev3
|
||||||
xcuserdata
|
xcuserdata
|
||||||
|
AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm
|
||||||
## Other
|
## Other
|
||||||
*.xccheckout
|
*.xccheckout
|
||||||
*.moved-aside
|
*.moved-aside
|
||||||
|
|||||||
2
.gitmodules
vendored
@@ -9,7 +9,7 @@
|
|||||||
url = https://github.com/libimobiledevice/libusbmuxd.git
|
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||||
[submodule "Dependencies/libplist"]
|
[submodule "Dependencies/libplist"]
|
||||||
path = Dependencies/libplist
|
path = Dependencies/libplist
|
||||||
url = https://github.com/libimobiledevice/libplist.git
|
url = https://github.com/SideStore/libplist.git
|
||||||
[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
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.518",
|
"blue" : "175",
|
||||||
"green" : "0.502",
|
"green" : "4",
|
||||||
"red" : "0.004"
|
"red" : "115"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.404",
|
"blue" : "150",
|
||||||
"green" : "0.322",
|
"green" : "3",
|
||||||
"red" : "0.008"
|
"red" : "99"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
{
|
|
||||||
"pins" : [
|
|
||||||
{
|
|
||||||
"identity" : "altsign",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/SideStore/AltSign",
|
|
||||||
"state" : {
|
|
||||||
"branch" : "master",
|
|
||||||
"revision" : "7e0e7edcf8fbc44ac1e35da3e9030a297aa18b84"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "appcenter-sdk-apple",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "8354a50fe01a7e54e196d3b5493b5ab53dd5866a",
|
|
||||||
"version" : "4.4.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "keychainaccess",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
|
|
||||||
"version" : "4.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "launchatlogin",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/sindresorhus/LaunchAtLogin.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41",
|
|
||||||
"version" : "4.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "nuke",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/kean/Nuke.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "9318d02a8a6d20af56505c9673261c1fd3b3aebe",
|
|
||||||
"version" : "7.6.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "openssl",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/krzyzanowskim/OpenSSL",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "033fcb41dac96b1b6effa945ca1f9ade002370b2",
|
|
||||||
"version" : "1.1.1501"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "plcrashreporter",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/microsoft/PLCrashReporter.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "6b27393cad517c067dceea85fadf050e70c4ceaa",
|
|
||||||
"version" : "1.10.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "semanticversion",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e",
|
|
||||||
"version" : "0.3.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "sparkle",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "286edd1fa22505a9e54d170e9fd07d775ea233f2",
|
|
||||||
"version" : "2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "stprivilegedtask",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/JoeMatt/STPrivilegedTask.git",
|
|
||||||
"state" : {
|
|
||||||
"branch" : "master",
|
|
||||||
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : 2
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>aps-environment</key>
|
|
||||||
<string>development</string>
|
|
||||||
<key>com.apple.developer.siri</key>
|
<key>com.apple.developer.siri</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ final 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
|
||||||
|
|
||||||
if let version = self.app.latestVersion
|
if let version = self.app.latestAvailableVersion
|
||||||
{
|
{
|
||||||
self.versionDescriptionTextView.text = version.localizedDescription
|
self.versionDescriptionTextView.text = version.localizedDescription
|
||||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
||||||
|
|||||||
@@ -217,8 +217,8 @@ final class AppViewController: UIViewController
|
|||||||
|
|
||||||
self._shouldResetLayout = false
|
self._shouldResetLayout = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let statusBarHeight = UIApplication.shared.statusBarFrame.height
|
let statusBarHeight = self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
||||||
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||||
|
|
||||||
let inset = 12 as CGFloat
|
let inset = 12 as CGFloat
|
||||||
@@ -323,7 +323,7 @@ final class AppViewController: UIViewController
|
|||||||
|
|
||||||
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
|
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
|
||||||
|
|
||||||
self.scrollView.scrollIndicatorInsets.top = statusBarHeight
|
self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight
|
||||||
|
|
||||||
// Adjust content offset + size.
|
// Adjust content offset + size.
|
||||||
let contentOffset = self.scrollView.contentOffset
|
let contentOffset = self.scrollView.contentOffset
|
||||||
@@ -384,7 +384,7 @@ private extension AppViewController
|
|||||||
button.progress = progress
|
button.progress = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
if let versionDate = self.app.latestVersion?.date, versionDate > Date()
|
if let versionDate = self.app.latestAvailableVersion?.date, versionDate > Date()
|
||||||
{
|
{
|
||||||
self.bannerView.button.countdownDate = versionDate
|
self.bannerView.button.countdownDate = versionDate
|
||||||
self.navigationBarDownloadButton.countdownDate = versionDate
|
self.navigationBarDownloadButton.countdownDate = versionDate
|
||||||
@@ -510,7 +510,7 @@ extension AppViewController
|
|||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error, opensLog: true)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,14 +90,21 @@ private extension AppIDsViewController
|
|||||||
cell.bannerView.button.isUserInteractionEnabled = false
|
cell.bannerView.button.isUserInteractionEnabled = false
|
||||||
|
|
||||||
cell.bannerView.buttonLabel.isHidden = false
|
cell.bannerView.buttonLabel.isHidden = false
|
||||||
|
|
||||||
let currentDate = Date()
|
let currentDate = Date()
|
||||||
|
|
||||||
let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
|
let formatter = DateComponentsFormatter()
|
||||||
let numberOfDaysText = (numberOfDays == 1) ? NSLocalizedString("1 day", comment: "") : String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
|
formatter.unitsStyle = .full
|
||||||
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
|
formatter.includesApproximationPhrase = false
|
||||||
|
formatter.includesTimeRemainingPhrase = false
|
||||||
|
formatter.allowedUnits = [.minute, .hour, .day]
|
||||||
|
formatter.maximumUnitCount = 1
|
||||||
|
|
||||||
attributedAccessibilityLabel.mutableString.append(String(format: NSLocalizedString("Expires in %@.", comment: ""), numberOfDaysText) + " ")
|
cell.bannerView.button.setTitle((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")).uppercased(), for: .normal)
|
||||||
|
|
||||||
|
// formatter.includesTimeRemainingPhrase = true
|
||||||
|
|
||||||
|
// attributedAccessibilityLabel.mutableString.append((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " ")
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
// 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
|
||||||
{
|
{
|
||||||
@@ -380,7 +382,7 @@ private extension AppDelegate
|
|||||||
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, let version = storeApp.version else { continue }
|
guard let storeApp = update.storeApp, let version = storeApp.latestSupportedVersion else { continue }
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = NSLocalizedString("New Update Available", comment: "")
|
content.title = NSLocalizedString("New Update Available", comment: "")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
<scene sceneID="lNR-II-WoW">
|
<scene sceneID="lNR-II-WoW">
|
||||||
<objects>
|
<objects>
|
||||||
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
|
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<color key="barTintColor" name="SettingsBackground"/>
|
<color key="barTintColor" name="SettingsBackground"/>
|
||||||
@@ -36,19 +36,19 @@
|
|||||||
<!--Authentication View Controller-->
|
<!--Authentication View Controller-->
|
||||||
<scene sceneID="OCd-xc-Ms7">
|
<scene sceneID="OCd-xc-Ms7">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
|
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
|
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
|
||||||
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
|
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
|
||||||
</view>
|
</view>
|
||||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
|
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
|
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="603"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
|
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
|
||||||
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
|
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="332" height="41"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="41"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
|
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
|
||||||
<rect key="frame" x="16" y="518.5" width="343" height="96.5"/>
|
<rect key="frame" x="16" y="498.5" width="343" height="96.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
|
||||||
@@ -198,6 +198,10 @@
|
|||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
|
||||||
|
<constraint firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
|
||||||
|
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
|
||||||
<constraint firstItem="DBk-rT-ZE8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="YmX-7v-pxh" secondAttribute="bottom" constant="8" symbolic="YES" id="zTU-eY-DWd"/>
|
<constraint firstItem="DBk-rT-ZE8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="YmX-7v-pxh" secondAttribute="bottom" constant="8" symbolic="YES" id="zTU-eY-DWd"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
@@ -215,19 +219,15 @@
|
|||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
|
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
|
||||||
<constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
|
<constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
|
||||||
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
|
|
||||||
<constraint firstItem="oyW-Fd-ojD" firstAttribute="top" secondItem="zMn-DV-fpy" secondAttribute="top" id="730-db-ukB"/>
|
<constraint firstItem="oyW-Fd-ojD" firstAttribute="top" secondItem="zMn-DV-fpy" secondAttribute="top" id="730-db-ukB"/>
|
||||||
<constraint firstItem="2wp-qG-f0Z" firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
|
|
||||||
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="oyW-Fd-ojD" secondAttribute="trailing" id="KGE-CN-SWf"/>
|
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="oyW-Fd-ojD" secondAttribute="trailing" id="KGE-CN-SWf"/>
|
||||||
<constraint firstItem="WXx-hX-AXv" firstAttribute="top" secondItem="mjy-4S-hyH" secondAttribute="top" id="LPQ-bF-ic0"/>
|
<constraint firstItem="WXx-hX-AXv" firstAttribute="top" secondItem="mjy-4S-hyH" secondAttribute="top" id="LPQ-bF-ic0"/>
|
||||||
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="WXx-hX-AXv" secondAttribute="trailing" id="MG7-A6-pKp"/>
|
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="WXx-hX-AXv" secondAttribute="trailing" id="MG7-A6-pKp"/>
|
||||||
<constraint firstAttribute="trailingMargin" secondItem="YmX-7v-pxh" secondAttribute="trailing" id="O4T-nu-o3e"/>
|
<constraint firstAttribute="trailingMargin" secondItem="YmX-7v-pxh" secondAttribute="trailing" id="O4T-nu-o3e"/>
|
||||||
<constraint firstItem="zMn-DV-fpy" firstAttribute="bottom" secondItem="oyW-Fd-ojD" secondAttribute="bottom" id="PuX-ab-cEq"/>
|
<constraint firstItem="zMn-DV-fpy" firstAttribute="bottom" secondItem="oyW-Fd-ojD" secondAttribute="bottom" id="PuX-ab-cEq"/>
|
||||||
<constraint firstItem="oyW-Fd-ojD" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="SzC-gC-Nvi"/>
|
<constraint firstItem="oyW-Fd-ojD" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="SzC-gC-Nvi"/>
|
||||||
<constraint firstItem="2wp-qG-f0Z" firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
|
|
||||||
<constraint firstItem="WXx-hX-AXv" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="d08-zF-5X6"/>
|
<constraint firstItem="WXx-hX-AXv" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="d08-zF-5X6"/>
|
||||||
<constraint firstItem="2wp-qG-f0Z" firstAttribute="height" secondItem="oyW-Fd-ojD" secondAttribute="height" id="dFN-pw-TWt"/>
|
<constraint firstItem="2wp-qG-f0Z" firstAttribute="height" secondItem="oyW-Fd-ojD" secondAttribute="height" id="dFN-pw-TWt"/>
|
||||||
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
|
|
||||||
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
|
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
@@ -258,13 +258,13 @@
|
|||||||
<!--How it works-->
|
<!--How it works-->
|
||||||
<scene sceneID="dMt-EA-SGy">
|
<scene sceneID="dMt-EA-SGy">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
|
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
|
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
|
||||||
<rect key="frame" x="0.0" y="44" width="375" height="564"/>
|
<rect key="frame" x="0.0" y="64" width="375" height="544"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
|
||||||
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
|
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
|
||||||
@@ -298,7 +298,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
|
||||||
<rect key="frame" x="16" y="168" width="343" height="95.5"/>
|
<rect key="frame" x="16" y="161" width="343" height="95.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
@@ -310,7 +310,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
|
||||||
<rect key="frame" x="79" y="17" width="264" height="61.5"/>
|
<rect key="frame" x="79" y="17.5" width="264" height="60.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||||
@@ -319,7 +319,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
|
||||||
<rect key="frame" x="0.0" y="25.5" width="264" height="36"/>
|
<rect key="frame" x="0.0" y="25.5" width="264" height="35"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
|
||||||
<rect key="frame" x="16" y="300.5" width="343" height="95.5"/>
|
<rect key="frame" x="16" y="287.5" width="343" height="95.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
|
||||||
<rect key="frame" x="79" y="16" width="264" height="64"/>
|
<rect key="frame" x="79" y="15.5" width="264" height="64"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||||
@@ -360,7 +360,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
|
||||||
<rect key="frame" x="16" y="433.5" width="343" height="95.5"/>
|
<rect key="frame" x="16" y="413.5" width="343" height="95.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
@@ -434,7 +434,7 @@
|
|||||||
<!--Refresh AltStore-->
|
<!--Refresh AltStore-->
|
||||||
<scene sceneID="9Vh-dM-OqX">
|
<scene sceneID="9Vh-dM-OqX">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
|
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
@@ -445,7 +445,7 @@
|
|||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
|
||||||
<rect key="frame" x="16" y="570" width="343" height="89"/>
|
<rect key="frame" x="16" y="570" width="343" height="89"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
|
||||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
@@ -493,12 +493,12 @@
|
|||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="2967" y="736"/>
|
<point key="canvasLocation" x="3025" y="734"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Select a Team-->
|
<!--Select a Team-->
|
||||||
<scene sceneID="ioQ-WB-CLJ">
|
<scene sceneID="ioQ-WB-CLJ">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH">
|
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
@@ -506,11 +506,11 @@
|
|||||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<prototypes>
|
<prototypes>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
|
<rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="334" height="60"/>
|
<rect key="frame" x="0.0" y="0.0" width="334.5" height="60"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM">
|
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM">
|
||||||
@@ -550,20 +550,19 @@
|
|||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1401" y="734"/>
|
<point key="canvasLocation" x="2114" y="734"/>
|
||||||
|
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<color key="tintColor" name="Primary"/>
|
<color key="tintColor" name="Primary"/>
|
||||||
<resources>
|
<resources>
|
||||||
<namedColor name="Primary">
|
<namedColor name="Primary">
|
||||||
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<namedColor name="SettingsBackground">
|
<namedColor name="SettingsBackground">
|
||||||
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<namedColor name="SettingsHighlighted">
|
<namedColor name="SettingsHighlighted">
|
||||||
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.38823529411764707" green="0.011764705882352941" blue="0.58823529411764708" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
@@ -108,11 +108,9 @@ private extension AuthenticationViewController
|
|||||||
|
|
||||||
case .failure(let error as NSError):
|
case .failure(let error as NSError):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: ""))
|
let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: ""))
|
||||||
|
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.textLabel.textColor = .altPink
|
|
||||||
toastView.detailTextLabel.textColor = .altPink
|
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
self.toastView = toastView
|
self.toastView = toastView
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21223" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21204"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
@@ -356,8 +356,8 @@
|
|||||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="ewH-gi-pyW">
|
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="ewH-gi-pyW">
|
||||||
<rect key="frame" x="0.0" y="30.5" width="335" height="17"/>
|
<rect key="frame" x="0.0" y="30.5" width="335" height="17"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Version 4.4.2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7E0-TV-G4l">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Version 0.5.6" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7E0-TV-G4l">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="84.5" height="17"/>
|
<rect key="frame" x="0.0" y="0.0" width="84" height="17"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -596,7 +596,7 @@ World</string>
|
|||||||
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
|
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<color key="tintColor" name="Primary"/>
|
<color key="tintColor" name="Primary"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
@@ -626,7 +626,7 @@ World</string>
|
|||||||
</tabBarItem>
|
</tabBarItem>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@@ -883,7 +883,7 @@ World</string>
|
|||||||
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
|
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
|
||||||
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
|
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
|
||||||
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
|
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
|
||||||
<rect key="frame" x="16" y="1" width="83" height="42"/>
|
<rect key="frame" x="16" y="7" width="83" height="42"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
</view>
|
</view>
|
||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
@@ -909,7 +909,7 @@ World</string>
|
|||||||
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
|
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
@@ -928,7 +928,7 @@ World</string>
|
|||||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@@ -1070,7 +1070,7 @@ World</string>
|
|||||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@@ -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="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<namedColor name="BlurTint">
|
<namedColor name="BlurTint">
|
||||||
<color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<namedColor name="Primary">
|
<namedColor name="Primary">
|
||||||
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" 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"/>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
import minimuxer
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@@ -113,9 +114,9 @@ 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 let versionDate = app.latestVersion?.date, versionDate > Date()
|
if let versionDate = app.latestSupportedVersion?.date, versionDate > Date()
|
||||||
{
|
{
|
||||||
cell.bannerView.button.countdownDate = app.versionDate
|
cell.bannerView.button.countdownDate = versionDate
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -264,14 +265,20 @@ private extension BrowseViewController
|
|||||||
previousProgress?.cancel()
|
previousProgress?.cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !minimuxer.ready() {
|
||||||
|
let toastView = ToastView(error: MinimuxerError.NoConnection)
|
||||||
|
toastView.show(in: self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in
|
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .failure(OperationError.cancelled): break // Ignore
|
case .failure(OperationError.cancelled): break // Ignore
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error, opensLog: true)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
|
|
||||||
case .success: print("Installed app:", app.bundleIdentifier)
|
case .success: print("Installed app:", app.bundleIdentifier)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class CollapsingTextView: UITextView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lineSpacing: CGFloat = 2 {
|
var lineSpacing: Double = 2 {
|
||||||
didSet {
|
didSet {
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,19 @@ final class CollapsingTextView: UITextView
|
|||||||
{
|
{
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
self.layoutManager.delegate = self
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
|
if #available(iOS 16, *)
|
||||||
|
{
|
||||||
|
self.updateText()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.layoutManager.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
self.textContainerInset = .zero
|
self.textContainerInset = .zero
|
||||||
self.textContainer.lineFragmentPadding = 0
|
self.textContainer.lineFragmentPadding = 0
|
||||||
@@ -108,6 +120,25 @@ private extension CollapsingTextView
|
|||||||
{
|
{
|
||||||
self.isCollapsed.toggle()
|
self.isCollapsed.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 16, *)
|
||||||
|
func updateText()
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let style = NSMutableParagraphStyle()
|
||||||
|
style.lineSpacing = self.lineSpacing
|
||||||
|
|
||||||
|
var attributedText = try AttributedString(self.attributedText, including: \.uiKit)
|
||||||
|
attributedText[AttributeScopes.UIKitAttributes.ParagraphStyleAttribute.self] = style
|
||||||
|
|
||||||
|
self.attributedText = NSAttributedString(attributedText)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("[ALTLog] Failed to update CollapsingTextView line spacing:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CollapsingTextView: NSLayoutManagerDelegate
|
extension CollapsingTextView: NSLayoutManagerDelegate
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
extension PillButton
|
||||||
|
{
|
||||||
|
static let minimumSize = CGSize(width: 77, height: 31)
|
||||||
|
static let contentInsets = NSDirectionalEdgeInsets(top: 7, leading: 13, bottom: 7, trailing: 13)
|
||||||
|
}
|
||||||
|
|
||||||
final class PillButton: UIButton
|
final class PillButton: UIButton
|
||||||
{
|
{
|
||||||
override var accessibilityValue: String? {
|
override var accessibilityValue: String? {
|
||||||
@@ -70,9 +76,7 @@ final class PillButton: UIButton
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
override var intrinsicContentSize: CGSize {
|
override var intrinsicContentSize: CGSize {
|
||||||
var size = super.intrinsicContentSize
|
let size = self.sizeThatFits(CGSize(width: Double.infinity, height: .infinity))
|
||||||
size.width += 26
|
|
||||||
size.height += 3
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +92,8 @@ final class PillButton: UIButton
|
|||||||
self.layer.masksToBounds = true
|
self.layer.masksToBounds = true
|
||||||
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
|
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
|
||||||
|
|
||||||
|
self.contentEdgeInsets = UIEdgeInsets(top: Self.contentInsets.top, left: Self.contentInsets.leading, bottom: Self.contentInsets.bottom, right: Self.contentInsets.trailing)
|
||||||
|
|
||||||
self.activityIndicatorView.style = .medium
|
self.activityIndicatorView.style = .medium
|
||||||
self.activityIndicatorView.isUserInteractionEnabled = false
|
self.activityIndicatorView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
@@ -119,6 +125,15 @@ final class PillButton: UIButton
|
|||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func sizeThatFits(_ size: CGSize) -> CGSize
|
||||||
|
{
|
||||||
|
var size = super.sizeThatFits(size)
|
||||||
|
size.width = max(size.width, PillButton.minimumSize.width)
|
||||||
|
size.height = max(size.height, PillButton.minimumSize.height)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension PillButton
|
private extension PillButton
|
||||||
|
|||||||
@@ -18,8 +18,17 @@ extension TimeInterval
|
|||||||
|
|
||||||
final class ToastView: RSTToastView
|
final class ToastView: RSTToastView
|
||||||
{
|
{
|
||||||
|
static let openErrorLogNotification = Notification.Name("ALTOpenErrorLogNotification")
|
||||||
|
|
||||||
var preferredDuration: TimeInterval
|
var preferredDuration: TimeInterval
|
||||||
|
|
||||||
|
var opensErrorLog: Bool = false
|
||||||
|
|
||||||
|
convenience init(text: String, detailText: String?, opensLog: Bool = false) {
|
||||||
|
self.init(text: text, detailText: detailText)
|
||||||
|
self.opensErrorLog = opensLog
|
||||||
|
}
|
||||||
|
|
||||||
override init(text: String, detailText detailedText: String?)
|
override init(text: String, detailText detailedText: String?)
|
||||||
{
|
{
|
||||||
if detailedText == nil
|
if detailedText == nil
|
||||||
@@ -43,53 +52,43 @@ final class ToastView: RSTToastView
|
|||||||
// RSTToastView does not expose stack view containing labels,
|
// RSTToastView does not expose stack view containing labels,
|
||||||
// so we access it indirectly as the labels' superview.
|
// so we access it indirectly as the labels' superview.
|
||||||
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
|
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
|
||||||
|
stackView.alignment = .leading
|
||||||
}
|
}
|
||||||
|
self.addTarget(self, action: #selector(ToastView.showErrorLog), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convenience init(error: Error, opensLog: Bool = false) {
|
||||||
|
self.init(error: error)
|
||||||
|
self.opensErrorLog = opensLog
|
||||||
|
}
|
||||||
|
|
||||||
convenience init(error: Error)
|
convenience init(error: Error)
|
||||||
{
|
{
|
||||||
var error = error as NSError
|
var error = error as NSError
|
||||||
var underlyingError = error.underlyingError
|
var underlyingError = error.underlyingError
|
||||||
|
|
||||||
var preferredDuration: TimeInterval?
|
|
||||||
|
|
||||||
if
|
if
|
||||||
let unwrappedUnderlyingError = underlyingError,
|
let unwrappedUnderlyingError = underlyingError,
|
||||||
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
|
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
|
||||||
{
|
{
|
||||||
// Treat underlyingError as the primary error.
|
// Treat underlyingError as the primary error, but keep localized title + failure.
|
||||||
|
let nsError = error as NSError
|
||||||
error = unwrappedUnderlyingError as NSError
|
error = unwrappedUnderlyingError as NSError
|
||||||
|
|
||||||
|
if let localizedTitle = nsError.localizedTitle {
|
||||||
|
error = error.withLocalizedTitle(localizedTitle)
|
||||||
|
}
|
||||||
|
if let localizedFailure = nsError.localizedFailure {
|
||||||
|
error = error.withLocalizedFailure(localizedFailure)
|
||||||
|
}
|
||||||
|
|
||||||
underlyingError = nil
|
underlyingError = nil
|
||||||
|
|
||||||
preferredDuration = .longToastViewDuration
|
|
||||||
}
|
}
|
||||||
|
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
|
||||||
let text: String
|
let detailText = error.localizedDescription
|
||||||
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.init(text: text, detailText: detailText)
|
self.init(text: text, detailText: detailText)
|
||||||
|
|
||||||
if let preferredDuration = preferredDuration
|
|
||||||
{
|
|
||||||
self.preferredDuration = preferredDuration
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init(coder aDecoder: NSCoder) {
|
required init(coder aDecoder: NSCoder) {
|
||||||
@@ -112,6 +111,18 @@ final class ToastView: RSTToastView
|
|||||||
|
|
||||||
override func show(in view: UIView, duration: TimeInterval)
|
override func show(in view: UIView, duration: TimeInterval)
|
||||||
{
|
{
|
||||||
|
if opensErrorLog, #available(iOS 13.0, *), case let configuration = UIImage.SymbolConfiguration(font: self.textLabel.font),
|
||||||
|
let icon = UIImage(systemName: "chevron.right.circle", withConfiguration: configuration) {
|
||||||
|
let tintedIcon = icon.withTintColor(.white, renderingMode: .alwaysOriginal)
|
||||||
|
let moreIconImageView = UIImageView(image: tintedIcon)
|
||||||
|
moreIconImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.addSubview(moreIconImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
moreIconImageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -self.layoutMargins.right),
|
||||||
|
moreIconImageView.centerYAnchor.constraint(equalTo: self.textLabel.centerYAnchor),
|
||||||
|
moreIconImageView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.textLabel.trailingAnchor, multiplier: 1.0)
|
||||||
|
])
|
||||||
|
}
|
||||||
super.show(in: view, duration: duration)
|
super.show(in: view, duration: duration)
|
||||||
|
|
||||||
let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
|
let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
|
||||||
@@ -127,4 +138,10 @@ final class ToastView: RSTToastView
|
|||||||
{
|
{
|
||||||
self.show(in: view, duration: self.preferredDuration)
|
self.show(in: view, duration: self.preferredDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func showErrorLog() {
|
||||||
|
guard self.opensErrorLog else { return }
|
||||||
|
NotificationCenter.default.post(name: ToastView.openErrorLogNotification, object: self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>ALTAnisetteURL</key>
|
||||||
|
<string>https://ani.sidestore.io</string>
|
||||||
<key>ALTAppGroups</key>
|
<key>ALTAppGroups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||||
@@ -9,12 +11,10 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>ALTDeviceID</key>
|
<key>ALTDeviceID</key>
|
||||||
<string>00008101-000129D63698001E</string>
|
<string>00008101-000129D63698001E</string>
|
||||||
<key>ALTServerID</key>
|
|
||||||
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
|
||||||
<key>ALTPairingFile</key>
|
<key>ALTPairingFile</key>
|
||||||
<string><insert pairing file here></string>
|
<string><insert pairing file here></string>
|
||||||
<key>ALTAnisetteURL</key>
|
<key>ALTServerID</key>
|
||||||
<string>https://ani.sidestore.io</string>
|
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
@@ -44,8 +44,6 @@
|
|||||||
<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>
|
||||||
@@ -93,6 +91,13 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_altserver._tcp</string>
|
<string>_altserver._tcp</string>
|
||||||
@@ -131,13 +136,10 @@
|
|||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
@@ -204,7 +206,5 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>UIFileSharingEnabled</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import minimuxer
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
@@ -39,8 +40,12 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
|
|||||||
|
|
||||||
// Give ourselves 9 extra seconds before starting handle() timeout timer.
|
// Give ourselves 9 extra seconds before starting handle() timeout timer.
|
||||||
// 10 seconds or longer results in timeout regardless.
|
// 10 seconds or longer results in timeout regardless.
|
||||||
self.queue.asyncAfter(deadline: .now() + 9.0) {
|
self.queue.asyncAfter(deadline: .now() + 8.0) {
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
if minimuxer.ready() {
|
||||||
|
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||||
|
} else {
|
||||||
|
self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !DatabaseManager.shared.isStarted
|
if !DatabaseManager.shared.isStarted
|
||||||
@@ -52,12 +57,14 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
||||||
self.refreshApps(intent: intent)
|
self.refreshApps(intent: intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
||||||
self.refreshApps(intent: intent)
|
self.refreshApps(intent: intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +90,11 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
|
|||||||
// We took too long to finish and return the final result,
|
// We took too long to finish and return the final result,
|
||||||
// so we'll now present a normal notification when finished.
|
// so we'll now present a normal notification when finished.
|
||||||
operation.presentsFinishedNotification = true
|
operation.presentsFinishedNotification = true
|
||||||
|
if minimuxer.ready() {
|
||||||
|
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||||
|
} else {
|
||||||
|
self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil))
|
self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil))
|
||||||
@@ -106,6 +118,8 @@ private extension IntentHandler
|
|||||||
{
|
{
|
||||||
// Queue response in case refreshing finishes after confirm() but before handle().
|
// Queue response in case refreshing finishes after confirm() but before handle().
|
||||||
self.queuedResponses[intent] = response
|
self.queuedResponses[intent] = response
|
||||||
|
|
||||||
|
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,10 +140,12 @@ private extension IntentHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||||
|
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||||
}
|
}
|
||||||
catch RefreshError.noInstalledApps
|
catch ~RefreshErrorCode.noInstalledApps
|
||||||
{
|
{
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||||
|
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||||
}
|
}
|
||||||
catch let error as NSError
|
catch let error as NSError
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import minimuxer
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
||||||
|
|
||||||
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
||||||
{
|
{
|
||||||
private var didFinishLaunching = false
|
private var didFinishLaunching = false
|
||||||
@@ -47,6 +49,43 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(true)
|
super.viewDidAppear(true)
|
||||||
|
if #available(iOS 17, *), !UserDefaults.standard.sidejitenable {
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
self.isSideJITServerDetected() { result in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result {
|
||||||
|
case .success():
|
||||||
|
let dialogMessage = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
|
||||||
|
|
||||||
|
// Create OK button with action handler
|
||||||
|
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
||||||
|
UserDefaults.standard.sidejitenable = true
|
||||||
|
})
|
||||||
|
|
||||||
|
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
|
||||||
|
//Add OK button to a dialog message
|
||||||
|
dialogMessage.addAction(ok)
|
||||||
|
dialogMessage.addAction(cancel)
|
||||||
|
|
||||||
|
// Present Alert to
|
||||||
|
self.present(dialogMessage, animated: true, completion: nil)
|
||||||
|
case .failure(_):
|
||||||
|
print("Cannot find sideJITServer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
self.askfornetwork()
|
||||||
|
}
|
||||||
|
print("SideJITServer Enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#if !targetEnvironment(simulator)
|
#if !targetEnvironment(simulator)
|
||||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||||
|
|
||||||
@@ -58,6 +97,46 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func askfornetwork() {
|
||||||
|
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||||
|
|
||||||
|
var SJSURL = address
|
||||||
|
|
||||||
|
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||||
|
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a network operation at launch to Refresh SideJITServer
|
||||||
|
let url = URL(string: "\(SJSURL)/re/")!
|
||||||
|
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
||||||
|
print(data)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||||
|
|
||||||
|
var SJSURL = address
|
||||||
|
|
||||||
|
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||||
|
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a network operation at launch to Refresh SideJITServer
|
||||||
|
let url = URL(string: SJSURL)!
|
||||||
|
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
||||||
|
if let error = error {
|
||||||
|
print("No SideJITServer on Network")
|
||||||
|
completion(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func fetchPairingFile() -> String? {
|
func fetchPairingFile() -> String? {
|
||||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
@@ -70,16 +149,17 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
fm.fileExists(atPath: appResourcePath.path),
|
fm.fileExists(atPath: appResourcePath.path),
|
||||||
let data = fm.contents(atPath: appResourcePath.path),
|
let data = fm.contents(atPath: appResourcePath.path),
|
||||||
let contents = String(data: data, encoding: .utf8),
|
let contents = String(data: data, encoding: .utf8),
|
||||||
!contents.isEmpty {
|
!contents.isEmpty,
|
||||||
|
!UserDefaults.standard.isPairingReset {
|
||||||
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
||||||
return contents
|
return contents
|
||||||
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
|
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{
|
||||||
print("Loaded ALTPairingFile from Info.plist")
|
print("Loaded ALTPairingFile from Info.plist")
|
||||||
return plistString
|
return plistString
|
||||||
} 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://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert)
|
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", 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
|
||||||
@@ -91,14 +171,33 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
documentPickerController.shouldShowFileExtensions = true
|
documentPickerController.shouldShowFileExtensions = true
|
||||||
documentPickerController.delegate = self
|
documentPickerController.delegate = self
|
||||||
self.present(documentPickerController, animated: true, completion: nil)
|
self.present(documentPickerController, animated: true, completion: nil)
|
||||||
|
UserDefaults.standard.isPairingReset = false
|
||||||
})
|
})
|
||||||
|
|
||||||
//Add OK button to a dialog message
|
//Add "help" button to take user to wiki
|
||||||
|
let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in
|
||||||
|
let wikiURL: String = "https://docs.sidestore.io/docs/getting-started/pairing-file"
|
||||||
|
if let url = URL(string: wikiURL) {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
sleep(2)
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add buttons to dialog message
|
||||||
|
dialogMessage.addAction(wikiOption)
|
||||||
dialogMessage.addAction(ok)
|
dialogMessage.addAction(ok)
|
||||||
|
|
||||||
// Present Alert to
|
// Present Alert to
|
||||||
self.present(dialogMessage, animated: true, completion: nil)
|
self.present(dialogMessage, animated: true, completion: nil)
|
||||||
|
|
||||||
|
let dialogMessage2 = UIAlertController(title: "Analytics", message: "This app contains anonymous analytics for research and project development. By continuing to use this app, you are consenting to this data collection", preferredStyle: .alert)
|
||||||
|
|
||||||
|
let ok2 = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in})
|
||||||
|
|
||||||
|
dialogMessage2.addAction(ok2)
|
||||||
|
self.present(dialogMessage2, animated: true, completion: nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,14 +224,11 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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")
|
||||||
}
|
}
|
||||||
@@ -148,22 +244,20 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
}
|
}
|
||||||
|
|
||||||
func start_minimuxer_threads(_ pairing_file: String) {
|
func start_minimuxer_threads(_ pairing_file: String) {
|
||||||
set_usbmuxd_socket()
|
target_minimuxer_address()
|
||||||
#if false // Retries
|
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||||
var res = start_minimuxer(pairing_file: pairing_file)
|
do {
|
||||||
var attempts = 10
|
try start(pairing_file, documentsDirectory)
|
||||||
while (attempts != 0 && res != 0) {
|
} catch {
|
||||||
print("start_minimuxer `res` != 0, retry #\(attempts)")
|
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
||||||
res = start_minimuxer(pairing_file: pairing_file)
|
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
||||||
attempts -= 1
|
|
||||||
}
|
}
|
||||||
#else
|
if #available(iOS 17, *) {
|
||||||
let res = start_minimuxer(pairing_file: pairing_file)
|
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
|
||||||
#endif
|
}
|
||||||
if res != 0 {
|
else {
|
||||||
displayError("minimuxer failed to start. Incorrect arguments were passed.")
|
start_auto_mounter(documentsDirectory)
|
||||||
}
|
}
|
||||||
auto_mount_dev_image()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
81
AltStore/Managing Apps/AppExtensionView.swift
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// AppExtensionView.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by June P on 8/17/24.
|
||||||
|
// Copyright © 2024 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import CAltSign
|
||||||
|
|
||||||
|
extension ALTApplication: Identifiable {}
|
||||||
|
|
||||||
|
struct AppExtensionView: View {
|
||||||
|
var extensions: Set<ALTApplication>
|
||||||
|
@State var selection: [ALTApplication] = []
|
||||||
|
|
||||||
|
var completion: (_ selection: [ALTApplication]) -> Any?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
List {
|
||||||
|
ForEach(self.extensions.sorted {
|
||||||
|
$0.bundleIdentifier < $1.bundleIdentifier
|
||||||
|
}, id: \.self) { item in
|
||||||
|
MultipleSelectionRow(title: item.bundleIdentifier, isSelected: !selection.contains(item)) {
|
||||||
|
if self.selection.contains(item) {
|
||||||
|
self.selection.removeAll(where: { $0 == item })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.selection.append(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("App Extensions")
|
||||||
|
.onDisappear {
|
||||||
|
_ = completion(selection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MultipleSelectionRow: View {
|
||||||
|
var title: String
|
||||||
|
var isSelected: Bool
|
||||||
|
var action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
SwiftUI.Button(action: self.action) {
|
||||||
|
HStack {
|
||||||
|
Text(self.title)
|
||||||
|
if self.isSelected {
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppExtensionViewHostingController: UIHostingController<AppExtensionView> {
|
||||||
|
|
||||||
|
|
||||||
|
var completion: Optional<(_ selection: [ALTApplication]) -> Any?> = nil
|
||||||
|
|
||||||
|
required init(extensions: Set<ALTApplication>, completion: @escaping (_ selection: [ALTApplication]) -> Any?) {
|
||||||
|
self.completion = completion
|
||||||
|
super.init(rootView: AppExtensionView(extensions: extensions, completion: completion))
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppExtensionViewHostingController: UIPopoverPresentationControllerDelegate {
|
||||||
|
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,14 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import Intents
|
import Intents
|
||||||
import Combine
|
import Combine
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
|
import minimuxer
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
@@ -37,17 +39,12 @@ final class AppManagerPublisher: ObservableObject
|
|||||||
fileprivate(set) var refreshProgress = [String: Progress]()
|
fileprivate(set) var refreshProgress = [String: Progress]()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool
|
|
||||||
{
|
|
||||||
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
final class AppManager
|
final class AppManager
|
||||||
{
|
{
|
||||||
static let shared = AppManager()
|
static let shared = AppManager()
|
||||||
|
|
||||||
private(set) var updatePatronsResult: Result<Void, Error>?
|
private(set) var updatePatronsResult: Result<Void, Error>?
|
||||||
|
|
||||||
private let operationQueue = OperationQueue()
|
private let operationQueue = OperationQueue()
|
||||||
private let serialOperationQueue = OperationQueue()
|
private let serialOperationQueue = OperationQueue()
|
||||||
|
|
||||||
@@ -307,6 +304,45 @@ extension AppManager
|
|||||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearAppCache(completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let clearAppCacheOperation = ClearAppCacheOperation()
|
||||||
|
clearAppCacheOperation.resultHandler = { result in
|
||||||
|
completion(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.run([clearAppCacheOperation], context: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func log(_ error: Error, operation: LoggedError.Operation, app: AppProtocol)
|
||||||
|
{
|
||||||
|
switch error {
|
||||||
|
case ~OperationError.Code.cancelled: return // Don't log cancelled events
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
// Sanitize NSError on same thread before performing background task.
|
||||||
|
let sanitizedError = (error as NSError).sanitizedForSerialization()
|
||||||
|
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||||
|
var app = 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: operation, context: context)
|
||||||
|
try context.save()
|
||||||
|
}
|
||||||
|
catch let saveError
|
||||||
|
{
|
||||||
|
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppManager
|
extension AppManager
|
||||||
@@ -359,7 +395,7 @@ extension AppManager
|
|||||||
case .success(let source): fetchedSources.insert(source)
|
case .success(let source): fetchedSources.insert(source)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let source = managedObjectContext.object(with: source.objectID) as! Source
|
let source = managedObjectContext.object(with: source.objectID) as! Source
|
||||||
source.error = (error as NSError).sanitizedForCoreData()
|
source.error = (error as NSError).sanitizedForSerialization()
|
||||||
errors[source] = error
|
errors[source] = error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,7 +483,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw context.error ?? OperationError.unknown }
|
guard let result = results.values.first else { throw context.error ?? OperationError.unknown() }
|
||||||
completionHandler(result)
|
completionHandler(result)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -466,7 +502,7 @@ extension AppManager
|
|||||||
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||||
{
|
{
|
||||||
guard let storeApp = app.storeApp else {
|
guard let storeApp = app.storeApp else {
|
||||||
completionHandler(.failure(OperationError.appNotFound))
|
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
|
||||||
return Progress.discreteProgress(totalUnitCount: 1)
|
return Progress.discreteProgress(totalUnitCount: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,7 +510,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown }
|
guard let result = results.values.first else { throw OperationError.unknown() }
|
||||||
completionHandler(result)
|
completionHandler(result)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -510,8 +546,8 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown }
|
guard let result = results.values.first else { throw OperationError.unknown() }
|
||||||
|
|
||||||
let installedApp = try result.get()
|
let installedApp = try result.get()
|
||||||
assert(installedApp.managedObjectContext != nil)
|
assert(installedApp.managedObjectContext != nil)
|
||||||
|
|
||||||
@@ -549,7 +585,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown }
|
guard let result = results.values.first else { throw OperationError.unknown() }
|
||||||
|
|
||||||
let installedApp = try result.get()
|
let installedApp = try result.get()
|
||||||
assert(installedApp.managedObjectContext != nil)
|
assert(installedApp.managedObjectContext != nil)
|
||||||
@@ -575,8 +611,8 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown }
|
guard let result = results.values.first else { throw OperationError.unknown() }
|
||||||
|
|
||||||
let installedApp = try result.get()
|
let installedApp = try result.get()
|
||||||
assert(installedApp.managedObjectContext != nil)
|
assert(installedApp.managedObjectContext != nil)
|
||||||
|
|
||||||
@@ -600,7 +636,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown }
|
guard let result = results.values.first else { throw OperationError.unknown() }
|
||||||
|
|
||||||
let installedApp = try result.get()
|
let installedApp = try result.get()
|
||||||
assert(installedApp.managedObjectContext != nil)
|
assert(installedApp.managedObjectContext != nil)
|
||||||
@@ -670,13 +706,20 @@ extension AppManager
|
|||||||
var installedApp: InstalledApp?
|
var installedApp: InstalledApp?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let appName = installedApp.name
|
||||||
let context = Context()
|
let context = Context()
|
||||||
context.installedApp = installedApp
|
context.installedApp = installedApp
|
||||||
|
|
||||||
|
|
||||||
let enableJITOperation = EnableJITOperation(context: context)
|
let enableJITOperation = EnableJITOperation(context: context)
|
||||||
enableJITOperation.resultHandler = { (result) in
|
enableJITOperation.resultHandler = { (result) in
|
||||||
completionHandler(result)
|
switch result {
|
||||||
|
case .success: completionHandler(.success(()))
|
||||||
|
case .failure(let nsError as NSError):
|
||||||
|
let localizedTitle = String(format: NSLocalizedString("Failed to enable JIT for %@", comment: ""), appName)
|
||||||
|
let error = nsError.withLocalizedTitle(localizedTitle)
|
||||||
|
self.log(error, operation: .enableJIT, app: installedApp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.run([enableJITOperation], context: context, requiresSerialQueue: true)
|
self.run([enableJITOperation], context: context, requiresSerialQueue: true)
|
||||||
@@ -754,6 +797,12 @@ extension AppManager
|
|||||||
let progress = self.refreshProgress[app.bundleIdentifier]
|
let progress = self.refreshProgress[app.bundleIdentifier]
|
||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
|
||||||
|
{
|
||||||
|
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
|
||||||
|
return isActivelyManaging
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppManager
|
extension AppManager
|
||||||
@@ -806,12 +855,18 @@ private extension AppManager
|
|||||||
|
|
||||||
return bundleIdentifier
|
return bundleIdentifier
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
var loggedErrorOperation: LoggedError.Operation {
|
||||||
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
|
switch self {
|
||||||
{
|
case .install: return .install
|
||||||
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
|
case .update: return .update
|
||||||
return isActivelyManaging
|
case .refresh: return .refresh
|
||||||
|
case .activate: return .activate
|
||||||
|
case .deactivate: return .deactivate
|
||||||
|
case .backup: return .backup
|
||||||
|
case .restore: return .restore
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
@@ -876,7 +931,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
|
||||||
@@ -946,12 +1003,100 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
DispatchQueue.main.schedule {
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.isIdleTimeoutDisableEnabled
|
||||||
|
}
|
||||||
performAppOperations()
|
performAppOperations()
|
||||||
|
DispatchQueue.main.schedule {
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeAppExtensions(from application: ALTApplication, extensions: Set<ALTApplication>, _ presentingViewController: UIViewController, 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? There are \(extensions.count) Extensions", 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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
|
||||||
|
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
for appExtension in selection
|
||||||
|
{
|
||||||
|
print("Deleting extension \(appExtension.bundleIdentifier)")
|
||||||
|
|
||||||
|
try FileManager.default.removeItem(at: appExtension.fileURL)
|
||||||
|
}
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let suiview = popoverContentController.view!
|
||||||
|
suiview.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
popoverContentController.modalPresentationStyle = .popover
|
||||||
|
|
||||||
|
if let popoverPresentationController = popoverContentController.popoverPresentationController {
|
||||||
|
popoverPresentationController.sourceView = presentingViewController.view
|
||||||
|
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
|
||||||
|
popoverPresentationController.delegate = popoverContentController
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
presentingViewController.present(popoverContentController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
presentingViewController.present(alertController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||||
{
|
{
|
||||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
@@ -1024,7 +1169,73 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
verifyOperation.addDependency(downloadOperation)
|
verifyOperation.addDependency(downloadOperation)
|
||||||
|
|
||||||
|
/* Remove App Extensions */
|
||||||
|
|
||||||
|
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let error = context.error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
guard case .install = appOperation else {
|
||||||
|
operation.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let extensions = context.app?.appExtensions else { throw OperationError.invalidParameters }
|
||||||
|
|
||||||
|
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
|
||||||
|
|
||||||
|
|
||||||
|
self?.removeAppExtensions(from: app, extensions: extensions, presentingViewController) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(): break
|
||||||
|
case .failure(let error): context.error = error
|
||||||
|
}
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
group.context.error = error
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAppExtensionsOperation.addDependency(verifyOperation)
|
||||||
|
|
||||||
|
|
||||||
|
/* Refresh Anisette Data */
|
||||||
|
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
||||||
|
refreshAnisetteDataOperation.resultHandler = { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): context.error = error
|
||||||
|
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshAnisetteDataOperation.addDependency(removeAppExtensionsOperation)
|
||||||
|
|
||||||
|
|
||||||
|
/* Fetch Provisioning Profiles */
|
||||||
|
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
||||||
|
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
|
||||||
|
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): context.error = error
|
||||||
|
case .success(let provisioningProfiles):
|
||||||
|
context.provisioningProfiles = provisioningProfiles
|
||||||
|
print("PROVISIONING PROFILES \(context.provisioningProfiles)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
|
||||||
|
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
|
||||||
|
|
||||||
|
|
||||||
/* Deactivate Apps (if necessary) */
|
/* Deactivate Apps (if necessary) */
|
||||||
let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||||
do
|
do
|
||||||
@@ -1040,6 +1251,12 @@ private extension AppManager
|
|||||||
{
|
{
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let profiles = context.provisioningProfiles else { throw OperationError.invalidParameters }
|
||||||
|
if !profiles.contains(where: { $1.isFreeProvisioningProfile == true }) {
|
||||||
|
operation.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
|
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
|
||||||
|
|
||||||
@@ -1059,8 +1276,7 @@ private extension AppManager
|
|||||||
operation.finish()
|
operation.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deactivateAppsOperation.addDependency(verifyOperation)
|
deactivateAppsOperation.addDependency(fetchProvisioningProfilesOperation)
|
||||||
|
|
||||||
|
|
||||||
/* Patch App */
|
/* Patch App */
|
||||||
let patchAppOperation = RSTAsyncBlockOperation { operation in
|
let patchAppOperation = RSTAsyncBlockOperation { operation in
|
||||||
@@ -1134,32 +1350,6 @@ private extension AppManager
|
|||||||
patchAppOperation.addDependency(deactivateAppsOperation)
|
patchAppOperation.addDependency(deactivateAppsOperation)
|
||||||
|
|
||||||
|
|
||||||
/* Refresh Anisette Data */
|
|
||||||
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
|
||||||
refreshAnisetteDataOperation.resultHandler = { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): context.error = error
|
|
||||||
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
refreshAnisetteDataOperation.addDependency(patchAppOperation)
|
|
||||||
|
|
||||||
|
|
||||||
/* Fetch Provisioning Profiles */
|
|
||||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
|
||||||
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
|
|
||||||
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): context.error = error
|
|
||||||
case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
|
|
||||||
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
|
|
||||||
|
|
||||||
|
|
||||||
/* Resign */
|
/* Resign */
|
||||||
let resignAppOperation = ResignAppOperation(context: context)
|
let resignAppOperation = ResignAppOperation(context: context)
|
||||||
resignAppOperation.resultHandler = { (result) in
|
resignAppOperation.resultHandler = { (result) in
|
||||||
@@ -1169,7 +1359,7 @@ private extension AppManager
|
|||||||
case .success(let resignedApp): context.resignedApp = resignedApp
|
case .success(let resignedApp): context.resignedApp = resignedApp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resignAppOperation.addDependency(fetchProvisioningProfilesOperation)
|
resignAppOperation.addDependency(patchAppOperation)
|
||||||
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
|
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
|
||||||
|
|
||||||
|
|
||||||
@@ -1212,7 +1402,57 @@ private extension AppManager
|
|||||||
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
||||||
installOperation.addDependency(sendAppOperation)
|
installOperation.addDependency(sendAppOperation)
|
||||||
|
|
||||||
let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, patchAppOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
|
let notificationRegistrationOperation = RSTAsyncBlockOperation { (operation) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
|
||||||
|
if let error = context.error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
guard let app = context.installedApp else { operation.finish(); return }
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "App Expiring Soon"
|
||||||
|
let formatter = DateComponentsFormatter()
|
||||||
|
formatter.unitsStyle = .full
|
||||||
|
formatter.includesApproximationPhrase = false
|
||||||
|
formatter.includesTimeRemainingPhrase = false
|
||||||
|
|
||||||
|
formatter.allowedUnits = [.day, .hour, .minute]
|
||||||
|
|
||||||
|
formatter.maximumUnitCount = 1
|
||||||
|
|
||||||
|
let scheduledDate = DateInterval(start: Date(), duration: 60 * 60 * 24 * 6)
|
||||||
|
|
||||||
|
|
||||||
|
guard let timeLeft = formatter.string(from: scheduledDate.end, to: app.expirationDate) else { operation.finish(); return }
|
||||||
|
|
||||||
|
content.body = "App \(app.name) is expiring in \(timeLeft). Open SideStore to refresh now"
|
||||||
|
|
||||||
|
var dateComponents = DateComponents()
|
||||||
|
dateComponents.calendar = Calendar.current
|
||||||
|
|
||||||
|
let trigger = UNCalendarNotificationTrigger(
|
||||||
|
dateMatching: dateComponents, repeats: true)
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: app.bundleIdentifier, content: content, trigger: trigger)
|
||||||
|
|
||||||
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
|
|
||||||
|
notificationCenter.add(request) {_ in
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationRegistrationOperation.addDependency(installOperation)
|
||||||
|
|
||||||
|
|
||||||
|
let operations = [downloadOperation, verifyOperation, removeAppExtensionsOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, deactivateAppsOperation, patchAppOperation, resignAppOperation, sendAppOperation, installOperation, notificationRegistrationOperation]
|
||||||
group.add(operations)
|
group.add(operations)
|
||||||
self.run(operations, context: group.context)
|
self.run(operations, context: group.context)
|
||||||
|
|
||||||
@@ -1245,14 +1485,21 @@ private extension AppManager
|
|||||||
case .success(let installedApp):
|
case .success(let installedApp):
|
||||||
completionHandler(.success(installedApp))
|
completionHandler(.success(installedApp))
|
||||||
|
|
||||||
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound):
|
case .failure(MinimuxerError.ProfileInstall):
|
||||||
|
completionHandler(.failure(OperationError.noWiFi))
|
||||||
|
|
||||||
|
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound(name: app.name)):
|
||||||
// Fall back to installation if AltServer doesn't support newer provisioning profile requests,
|
// Fall back to installation if AltServer doesn't support newer provisioning profile requests,
|
||||||
// OR if the cached app could not be found and we may need to redownload it.
|
// OR if the cached app could not be found and we may need to redownload it.
|
||||||
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
|
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
|
||||||
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
if minimuxer.ready() {
|
||||||
completionHandler(result)
|
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
||||||
|
completionHandler(result)
|
||||||
|
}
|
||||||
|
progress.addChild(installProgress, withPendingUnitCount: 40)
|
||||||
|
} else {
|
||||||
|
completionHandler(.failure(OperationError.noWiFi))
|
||||||
}
|
}
|
||||||
progress.addChild(installProgress, withPendingUnitCount: 40)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
@@ -1519,7 +1766,7 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let application = ALTApplication(fileURL: app.fileURL) else {
|
guard let application = ALTApplication(fileURL: app.fileURL) else {
|
||||||
completionHandler(.failure(OperationError.appNotFound))
|
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
|
||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1531,8 +1778,8 @@ private extension AppManager
|
|||||||
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
|
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
|
||||||
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound }
|
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound(name: app.name) }
|
||||||
|
|
||||||
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
|
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
|
||||||
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
|
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
|
||||||
|
|
||||||
@@ -1670,11 +1917,35 @@ private extension AppManager
|
|||||||
do { try installedApp.managedObjectContext?.save() }
|
do { try installedApp.managedObjectContext?.save() }
|
||||||
catch { print("Error saving installed app.", error) }
|
catch { print("Error saving installed app.", error) }
|
||||||
}
|
}
|
||||||
catch
|
catch let nsError as NSError
|
||||||
{
|
{
|
||||||
|
var appName: String!
|
||||||
|
if let app = operation.app as? (NSManagedObject & AppProtocol) {
|
||||||
|
if let context = app.managedObjectContext {
|
||||||
|
context.performAndWait {
|
||||||
|
appName = app.name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appName = NSLocalizedString("Unknown App", comment: "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appName = operation.app.name
|
||||||
|
}
|
||||||
|
|
||||||
|
let localizedTitle: String
|
||||||
|
switch operation {
|
||||||
|
case .install: localizedTitle = String(format: NSLocalizedString("Failed to Install %@", comment: ""), appName)
|
||||||
|
case .refresh: localizedTitle = String(format: NSLocalizedString("Failed to Refresh %@", comment: ""), appName)
|
||||||
|
case .update: localizedTitle = String(format: NSLocalizedString("Failed to Update %@", comment: ""), appName)
|
||||||
|
case .activate: localizedTitle = String(format: NSLocalizedString("Failed to Activate %@", comment: ""), appName)
|
||||||
|
case .deactivate: localizedTitle = String(format: NSLocalizedString("Failed to Deactivate %@", comment: ""), appName)
|
||||||
|
case .backup: localizedTitle = String(format: NSLocalizedString("Failed to Backup %@", comment: ""), appName)
|
||||||
|
case .restore: localizedTitle = String(format: NSLocalizedString("Failed to Restore %@ Backup", comment: ""), appName)
|
||||||
|
}
|
||||||
|
let error = nsError.withLocalizedTitle(localizedTitle)
|
||||||
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
|
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
|
||||||
|
|
||||||
self.log(error, for: operation)
|
self.log(error, operation: operation.loggedErrorOperation, app: operation.app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1691,51 +1962,15 @@ private extension AppManager
|
|||||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = NSLocalizedString("AltStore Expiring Soon", comment: "")
|
content.title = NSLocalizedString("SideStore Expiring Soon", comment: "")
|
||||||
content.body = NSLocalizedString("AltStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
|
content.body = NSLocalizedString("SideStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
|
||||||
content.sound = .default
|
content.sound = .default
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
|
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
|
||||||
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`
|
||||||
|
|||||||
@@ -22,13 +22,27 @@ extension AppManager
|
|||||||
|
|
||||||
var managedObjectContext: NSManagedObjectContext?
|
var managedObjectContext: NSManagedObjectContext?
|
||||||
|
|
||||||
var errorDescription: String? {
|
var localizedTitle: String? {
|
||||||
if let error = self.primaryError
|
var localizedTitle: String?
|
||||||
{
|
self.managedObjectContext?.performAndWait {
|
||||||
return error.localizedDescription
|
if self.sources?.count == 1 {
|
||||||
|
localizedTitle = NSLocalizedString("Failed to refresh Store", comment: "")
|
||||||
|
} else if self.errors.count == 1 {
|
||||||
|
guard let source = self.errors.keys.first else { return }
|
||||||
|
localizedTitle = String(format: NSLocalizedString("Failed to refresh Source '%@'", comment: ""), source.name)
|
||||||
|
} else {
|
||||||
|
localizedTitle = String(format: NSLocalizedString("Failed to refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
return localizedTitle
|
||||||
{
|
}
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
if let error = self.primaryError {
|
||||||
|
return error.localizedDescription
|
||||||
|
} else if let error = self.errors.values.first, self.errors.count == 1 {
|
||||||
|
return error.localizedDescription
|
||||||
|
} else {
|
||||||
var localizedDescription: String?
|
var localizedDescription: String?
|
||||||
|
|
||||||
self.managedObjectContext?.performAndWait {
|
self.managedObjectContext?.performAndWait {
|
||||||
@@ -67,8 +81,14 @@ extension AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
var errorUserInfo: [String : Any] {
|
var errorUserInfo: [String : Any] {
|
||||||
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
|
let errors = Array(self.errors.values)
|
||||||
return [NSUnderlyingErrorKey: error]
|
var userInfo = [String: Any]()
|
||||||
|
userInfo[ALTLocalizedTitleErrorKey] = self.localizedTitle
|
||||||
|
userInfo[NSUnderlyingErrorKey] = self.primaryError
|
||||||
|
if #available(iOS 14.5, *), !errors.isEmpty {
|
||||||
|
userInfo[NSMultipleUnderlyingErrorsKey] = errors
|
||||||
|
}
|
||||||
|
return userInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ error: Error)
|
init(_ error: Error)
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import UIKit
|
|||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import Intents
|
import Intents
|
||||||
import Combine
|
import Combine
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
@@ -153,6 +155,13 @@ final class MyAppsViewController: UICollectionViewController
|
|||||||
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
|
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
var minimuxerStatus: Bool {
|
||||||
|
guard minimuxer.ready() else {
|
||||||
|
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No WiFi or VPN!")).show(in: self)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension MyAppsViewController
|
private extension MyAppsViewController
|
||||||
@@ -186,7 +195,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?.latestVersion?.date, ascending: true),
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestSupportedVersion?.date, ascending: false),
|
||||||
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
|
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
|
||||||
fetchRequest.returnsObjectsAsFaults = false
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
|
||||||
@@ -195,21 +204,21 @@ 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, let latestVersion = app.latestVersion else { return }
|
guard let app = installedApp.storeApp, let latestSupportedVersion = app.latestSupportedVersion 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
|
||||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
cell.tintColor = app.tintColor ?? .altPrimary
|
cell.tintColor = app.tintColor ?? .altPrimary
|
||||||
cell.versionDescriptionTextView.text = app.versionDescription
|
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
|
||||||
|
|
||||||
cell.bannerView.iconImageView.image = nil
|
cell.bannerView.iconImageView.image = nil
|
||||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
|
|
||||||
cell.bannerView.configure(for: app)
|
cell.bannerView.configure(for: app)
|
||||||
|
|
||||||
let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter)
|
let versionDate = Date().relativeDateString(since: latestSupportedVersion.date, dateFormatter: self.dateFormatter)
|
||||||
cell.bannerView.subtitleLabel.text = versionDate
|
cell.bannerView.subtitleLabel.text = versionDate
|
||||||
|
|
||||||
let appName: String
|
let appName: String
|
||||||
@@ -223,7 +232,7 @@ private extension MyAppsViewController
|
|||||||
appName = app.name
|
appName = app.name
|
||||||
}
|
}
|
||||||
|
|
||||||
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate)
|
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestSupportedVersion.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)
|
||||||
@@ -327,21 +336,25 @@ private extension MyAppsViewController
|
|||||||
let currentDate = Date()
|
let currentDate = Date()
|
||||||
|
|
||||||
let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate)
|
let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate)
|
||||||
let numberOfDaysText: String
|
|
||||||
|
|
||||||
if numberOfDays == 1
|
let formatter = DateComponentsFormatter()
|
||||||
{
|
formatter.unitsStyle = .full
|
||||||
numberOfDaysText = NSLocalizedString("1 day", comment: "")
|
formatter.includesApproximationPhrase = false
|
||||||
}
|
formatter.includesTimeRemainingPhrase = false
|
||||||
else
|
|
||||||
{
|
formatter.allowedUnits = [.day, .hour, .minute]
|
||||||
numberOfDaysText = String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
|
|
||||||
}
|
formatter.maximumUnitCount = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cell.bannerView.button.setTitle(formatter.string(from: currentDate, to: installedApp.expirationDate)?.uppercased(), for: .normal)
|
||||||
|
|
||||||
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
|
|
||||||
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name)
|
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name)
|
||||||
|
|
||||||
cell.bannerView.accessibilityLabel? += ". " + String(format: NSLocalizedString("Expires in %@", comment: ""), numberOfDaysText)
|
formatter.includesTimeRemainingPhrase = true
|
||||||
|
|
||||||
|
cell.bannerView.accessibilityLabel? += ". " + (formatter.string(from: currentDate, to: installedApp.expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " "
|
||||||
|
|
||||||
// Make sure refresh button is correct size.
|
// Make sure refresh button is correct size.
|
||||||
cell.layoutIfNeeded()
|
cell.layoutIfNeeded()
|
||||||
@@ -522,11 +535,9 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
guard !failures.isEmpty else { return }
|
guard !failures.isEmpty else { return }
|
||||||
|
|
||||||
let toastView: ToastView
|
|
||||||
|
|
||||||
if let failure = failures.first, results.count == 1
|
if let failure = failures.first, results.count == 1
|
||||||
{
|
{
|
||||||
toastView = ToastView(error: failure.value)
|
ToastView(error: failure.value).show(in: self)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -544,11 +555,10 @@ private extension MyAppsViewController
|
|||||||
let error = failures.first?.value as NSError?
|
let error = failures.first?.value as NSError?
|
||||||
let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription
|
let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription
|
||||||
|
|
||||||
toastView = ToastView(text: localizedText, detailText: detailText)
|
let toastView = ToastView(text: localizedText, detailText: detailText, opensLog: true)
|
||||||
toastView.preferredDuration = 4.0
|
toastView.preferredDuration = 4.0
|
||||||
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
toastView.show(in: self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refreshGroup = nil
|
self.refreshGroup = nil
|
||||||
@@ -639,6 +649,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
@IBAction func refreshAllApps(_ sender: UIBarButtonItem)
|
@IBAction func refreshAllApps(_ sender: UIBarButtonItem)
|
||||||
{
|
{
|
||||||
|
guard minimuxerStatus else { return }
|
||||||
|
|
||||||
self.isRefreshingAllApps = true
|
self.isRefreshingAllApps = true
|
||||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||||
|
|
||||||
@@ -682,8 +694,7 @@ private extension MyAppsViewController
|
|||||||
self.collectionView.reloadItems(at: [indexPath])
|
self.collectionView.reloadItems(at: [indexPath])
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
|
|
||||||
self.collectionView.reloadItems(at: [indexPath])
|
self.collectionView.reloadItems(at: [indexPath])
|
||||||
|
|
||||||
@@ -701,18 +712,11 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
@IBAction func sideloadApp(_ sender: UIBarButtonItem)
|
@IBAction func sideloadApp(_ sender: UIBarButtonItem)
|
||||||
{
|
{
|
||||||
let supportedTypes: [String]
|
guard minimuxerStatus else { return }
|
||||||
|
|
||||||
|
let supportedTypes = UTType.types(tag: "ipa", tagClass: .filenameExtension, conformingTo: nil)
|
||||||
|
|
||||||
if let types = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "ipa" as CFString, nil)?.takeRetainedValue()
|
let documentPickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes, asCopy: true)
|
||||||
{
|
|
||||||
supportedTypes = (types as NSArray).map { $0 as! String }
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
supportedTypes = ["com.apple.itunes.ipa"] // Declared by the system.
|
|
||||||
}
|
|
||||||
|
|
||||||
let documentPickerViewController = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
|
|
||||||
documentPickerViewController.delegate = self
|
documentPickerViewController.delegate = self
|
||||||
self.present(documentPickerViewController, animated: true, completion: nil)
|
self.present(documentPickerViewController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
@@ -779,7 +783,7 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
|
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||||
let unzipAppOperation = BlockOperation {
|
let unzipAppOperation = BlockOperation {
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let error = context.error
|
if let error = context.error
|
||||||
@@ -811,38 +815,7 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
unzipAppOperation.addDependency(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)
|
|
||||||
progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
|
|
||||||
|
|
||||||
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
let installAppOperation = RSTAsyncBlockOperation { (operation) in
|
let installAppOperation = RSTAsyncBlockOperation { (operation) in
|
||||||
do
|
do
|
||||||
@@ -891,22 +864,23 @@ private extension MyAppsViewController
|
|||||||
completion(.failure((OperationError.cancelled)))
|
completion(.failure((OperationError.cancelled)))
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
|
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
installAppOperation.addDependency(unzipAppOperation)
|
||||||
|
|
||||||
progress.addChild(installProgress, withPendingUnitCount: 65)
|
progress.addChild(installProgress, withPendingUnitCount: 65)
|
||||||
installAppOperation.addDependency(removeAppExtensionsOperation)
|
|
||||||
|
|
||||||
self.sideloadingProgress = progress
|
self.sideloadingProgress = progress
|
||||||
self.sideloadingProgressView.progress = 0
|
self.sideloadingProgressView.progress = 0
|
||||||
self.sideloadingProgressView.isHidden = false
|
self.sideloadingProgressView.isHidden = false
|
||||||
self.sideloadingProgressView.observedProgress = self.sideloadingProgress
|
self.sideloadingProgressView.observedProgress = self.sideloadingProgress
|
||||||
|
|
||||||
let operations = [downloadOperation, unzipAppOperation, removeAppExtensionsOperation, installAppOperation].compactMap { $0 }
|
let operations = [downloadOperation, unzipAppOperation, installAppOperation].compactMap { $0 }
|
||||||
self.operationQueue.addOperations(operations, waitUntilFinished: false)
|
self.operationQueue.addOperations(operations, waitUntilFinished: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,49 +929,6 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
self.present(alertController, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension MyAppsViewController
|
private extension MyAppsViewController
|
||||||
@@ -1007,13 +938,14 @@ private extension MyAppsViewController
|
|||||||
UIApplication.shared.open(installedApp.openAppURL) { success in
|
UIApplication.shared.open(installedApp.openAppURL) { success in
|
||||||
guard !success else { return }
|
guard !success else { return }
|
||||||
|
|
||||||
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name))
|
ToastView(error: OperationError.openAppFailed(name: installedApp.name), opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func refresh(_ installedApp: InstalledApp)
|
func refresh(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
|
guard minimuxerStatus else { return }
|
||||||
|
|
||||||
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
|
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
|
||||||
guard previousProgress == nil else {
|
guard previousProgress == nil else {
|
||||||
previousProgress?.cancel()
|
previousProgress?.cancel()
|
||||||
@@ -1035,6 +967,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
func activate(_ installedApp: InstalledApp)
|
func activate(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
|
guard minimuxerStatus else { return }
|
||||||
|
|
||||||
func finish(_ result: Result<InstalledApp, Error>)
|
func finish(_ result: Result<InstalledApp, Error>)
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
@@ -1055,8 +989,7 @@ private extension MyAppsViewController
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
installedApp.isActive = false
|
installedApp.isActive = false
|
||||||
|
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1110,7 +1043,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil)
|
func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil)
|
||||||
{
|
{
|
||||||
guard installedApp.isActive else { return }
|
guard installedApp.isActive, minimuxerStatus else { return }
|
||||||
|
|
||||||
installedApp.isActive = false
|
installedApp.isActive = false
|
||||||
|
|
||||||
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
|
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
|
||||||
@@ -1123,13 +1057,12 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
print("Failed to activate app:", error)
|
print("Failed to deactivate app:", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
installedApp.isActive = true
|
installedApp.isActive = true
|
||||||
|
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1151,7 +1084,7 @@ private extension MyAppsViewController
|
|||||||
message = NSLocalizedString("This will also erase all backup data for this app.", comment: "")
|
message = NSLocalizedString("This will also erase all backup data for this app.", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
|
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
alertController.addAction(.cancel)
|
alertController.addAction(.cancel)
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
|
||||||
AppManager.shared.remove(installedApp) { (result) in
|
AppManager.shared.remove(installedApp) { (result) in
|
||||||
@@ -1160,8 +1093,7 @@ private extension MyAppsViewController
|
|||||||
case .success: break
|
case .success: break
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1172,6 +1104,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
func backup(_ installedApp: InstalledApp)
|
func backup(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
|
guard minimuxerStatus else { return }
|
||||||
|
|
||||||
let title = NSLocalizedString("Start Backup?", comment: "")
|
let title = NSLocalizedString("Start Backup?", comment: "")
|
||||||
let message = NSLocalizedString("This will replace any previous backups. Please leave SideStore open until the backup is complete.", comment: "")
|
let message = NSLocalizedString("This will replace any previous backups. Please leave SideStore open until the backup is complete.", comment: "")
|
||||||
|
|
||||||
@@ -1193,9 +1127,8 @@ private extension MyAppsViewController
|
|||||||
print("Failed to back up app:", error)
|
print("Failed to back up app:", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
|
|
||||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1211,6 +1144,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
func restore(_ installedApp: InstalledApp)
|
func restore(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
|
guard minimuxerStatus else { return }
|
||||||
|
|
||||||
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
|
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
|
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
|
||||||
alertController.addAction(.cancel)
|
alertController.addAction(.cancel)
|
||||||
@@ -1228,8 +1163,7 @@ private extension MyAppsViewController
|
|||||||
print("Failed to restore app:", error)
|
print("Failed to restore app:", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1246,8 +1180,11 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return }
|
guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return }
|
||||||
|
|
||||||
let documentPicker = UIDocumentPickerViewController(url: backupURL, in: .exportToService)
|
let documentPicker = UIDocumentPickerViewController(forExporting: [backupURL], asCopy: true)
|
||||||
documentPicker.delegate = self
|
|
||||||
|
// Don't set delegate to avoid conflicting with import callbacks.
|
||||||
|
// documentPicker.delegate = self
|
||||||
|
|
||||||
self.present(documentPicker, animated: true, completion: nil)
|
self.present(documentPicker, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1301,8 +1238,7 @@ private extension MyAppsViewController
|
|||||||
print("Failed to change app icon.", error)
|
print("Failed to change app icon.", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1311,14 +1247,28 @@ private extension MyAppsViewController
|
|||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
func enableJIT(for installedApp: InstalledApp)
|
func enableJIT(for installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
let sidejitenabled = UserDefaults.standard.sidejitenable
|
||||||
|
|
||||||
|
if #unavailable(iOS 17) {
|
||||||
|
guard minimuxerStatus else { return }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if #available(iOS 17, *), !sidejitenabled {
|
||||||
|
ToastView(error: (OperationError.tooNewError as NSError).withLocalizedTitle("No iOS 17 On Device JIT!"), opensLog: true).show(in: self)
|
||||||
|
AppManager.shared.log(OperationError.tooNewError, operation: .enableJIT, app: installedApp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
AppManager.shared.enableJIT(for: installedApp) { result in
|
AppManager.shared.enableJIT(for: installedApp) { result in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .success: break
|
case .success: break
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1465,7 +1415,7 @@ extension MyAppsViewController
|
|||||||
let registeredAppIDs = team.appIDs.count
|
let registeredAppIDs = team.appIDs.count
|
||||||
|
|
||||||
let maximumAppIDCount = 10
|
let maximumAppIDCount = 10
|
||||||
let remainingAppIDs = max(maximumAppIDCount - registeredAppIDs, 0)
|
let remainingAppIDs = maximumAppIDCount - registeredAppIDs
|
||||||
|
|
||||||
if remainingAppIDs == 1
|
if remainingAppIDs == 1
|
||||||
{
|
{
|
||||||
@@ -1476,7 +1426,7 @@ extension MyAppsViewController
|
|||||||
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
|
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
footerView.textLabel.isHidden = false
|
footerView.textLabel.isHidden = remainingAppIDs < 0
|
||||||
|
|
||||||
case .individual, .organization, .unknown: footerView.textLabel.isHidden = true
|
case .individual, .organization, .unknown: footerView.textLabel.isHidden = true
|
||||||
@unknown default: break
|
@unknown default: break
|
||||||
@@ -2050,15 +2000,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate
|
|||||||
{
|
{
|
||||||
guard let fileURL = urls.first else { return }
|
guard let fileURL = urls.first else { return }
|
||||||
|
|
||||||
switch controller.documentPickerMode
|
self.sideloadApp(at: fileURL) { (result) in
|
||||||
{
|
print("Sideloaded app at \(fileURL) with result:", result)
|
||||||
case .import, .open:
|
|
||||||
self.sideloadApp(at: fileURL) { (result) in
|
|
||||||
print("Sideloaded app at \(fileURL) with result:", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
case .exportToService, .moveToService: break
|
|
||||||
@unknown default: break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,9 +313,8 @@ private extension NewsViewController
|
|||||||
{
|
{
|
||||||
case .failure(OperationError.cancelled): break // Ignore
|
case .failure(OperationError.cancelled): break // Ignore
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let toastView = ToastView(error: error)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
toastView.show(in: self)
|
|
||||||
|
|
||||||
case .success: print("Installed app:", storeApp.bundleIdentifier)
|
case .success: print("Installed app:", storeApp.bundleIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,9 +390,9 @@ 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 let versionDate = storeApp.latestVersion?.date, versionDate > Date()
|
if let versionDate = storeApp.latestSupportedVersion?.date, versionDate > Date()
|
||||||
{
|
{
|
||||||
footerView.bannerView.button.countdownDate = storeApp.versionDate
|
footerView.bannerView.button.countdownDate = versionDate
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -426,6 +425,10 @@ extension NewsViewController: UICollectionViewDelegateFlowLayout
|
|||||||
return previousSize
|
return previousSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take layout margins into account.
|
||||||
|
self.prototypeCell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
self.prototypeCell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||||
NSLayoutConstraint.activate([widthConstraint])
|
NSLayoutConstraint.activate([widthConstraint])
|
||||||
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
|
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import Network
|
|||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
enum AuthenticationError: LocalizedError
|
typealias AuthenticationError = AuthenticationErrorCode.Error
|
||||||
|
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
|
||||||
{
|
{
|
||||||
case noTeam
|
case noTeam
|
||||||
case noCertificate
|
case noCertificate
|
||||||
@@ -22,11 +24,11 @@ enum AuthenticationError: LocalizedError
|
|||||||
case missingPrivateKey
|
case missingPrivateKey
|
||||||
case missingCertificate
|
case missingCertificate
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorFailureReason: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams?", comment: "")
|
||||||
|
case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
|
||||||
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
|
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
|
||||||
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
|
||||||
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
||||||
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
||||||
}
|
}
|
||||||
@@ -212,8 +214,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
|||||||
guard
|
guard
|
||||||
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
|
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
|
||||||
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
||||||
else { throw AuthenticationError.noTeam }
|
else { throw AuthenticationError(.noTeam) }
|
||||||
|
|
||||||
// Account
|
// Account
|
||||||
account.isActiveAccount = true
|
account.isActiveAccount = true
|
||||||
|
|
||||||
@@ -431,7 +433,7 @@ private extension AuthenticationOperation
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
completionHandler(.failure(error ?? OperationError.unknown))
|
completionHandler(.failure(error ?? OperationError.unknown()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,7 +450,7 @@ private extension AuthenticationOperation
|
|||||||
if let team = teams.first {
|
if let team = teams.first {
|
||||||
return completionHandler(.success(team))
|
return completionHandler(.success(team))
|
||||||
} else {
|
} else {
|
||||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
return completionHandler(.failure(AuthenticationError(.noTeam)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@@ -459,7 +461,7 @@ private extension AuthenticationOperation
|
|||||||
|
|
||||||
if !self.present(selectTeamViewController)
|
if !self.present(selectTeamViewController)
|
||||||
{
|
{
|
||||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
return completionHandler(.failure(AuthenticationError(.noTeam)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,20 +490,20 @@ private extension AuthenticationOperation
|
|||||||
{
|
{
|
||||||
func requestCertificate()
|
func requestCertificate()
|
||||||
{
|
{
|
||||||
let machineName = "AltStore - " + UIDevice.current.name
|
let machineName: String = "SideStore - \(team.account.firstName)'s \(UIDevice.current.name)"
|
||||||
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
|
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let certificate = try Result(certificate, error).get()
|
let certificate = try Result(certificate, error).get()
|
||||||
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
|
guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) }
|
||||||
|
|
||||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let certificates = try Result(certificates, error).get()
|
let certificates = try Result(certificates, error).get()
|
||||||
|
|
||||||
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
|
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
|
||||||
throw AuthenticationError.missingCertificate
|
throw AuthenticationError(.missingCertificate)
|
||||||
}
|
}
|
||||||
|
|
||||||
certificate.privateKey = privateKey
|
certificate.privateKey = privateKey
|
||||||
@@ -522,16 +524,50 @@ private extension AuthenticationOperation
|
|||||||
|
|
||||||
func replaceCertificate(from certificates: [ALTCertificate])
|
func replaceCertificate(from certificates: [ALTCertificate])
|
||||||
{
|
{
|
||||||
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
|
let ourCertificates = certificates.filter { a in
|
||||||
|
a.machineName?.starts(with: "SideStore") == true || a.machineName?.starts(with: "AltStore") == true
|
||||||
|
}
|
||||||
|
|
||||||
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
if ourCertificates.isEmpty {
|
||||||
if let error = error, !success
|
return requestCertificate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have private keys for any of the certificates,
|
||||||
|
// so we need to revoke one and create a new one.
|
||||||
|
var certsText = ""
|
||||||
|
for certificate in ourCertificates {
|
||||||
|
if let name = certificate.machineName {
|
||||||
|
certsText.append("\(name)\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("Would you like to revoke your previous certificates?\n\(certsText)", comment: ""), message: nil, preferredStyle: .alert)
|
||||||
|
|
||||||
|
let noAction = UIAlertAction(title: NSLocalizedString("No", comment: ""), style: .default) { (action) in
|
||||||
|
requestCertificate()
|
||||||
|
}
|
||||||
|
let yesAction = UIAlertAction(title: NSLocalizedString("Yes", comment: ""), style: .default) { (action) in
|
||||||
|
for certificate in ourCertificates {
|
||||||
|
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
||||||
|
if let error = error, !success
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestCertificate()
|
||||||
|
}
|
||||||
|
alertController.addAction(noAction)
|
||||||
|
alertController.addAction(yesAction)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if self.navigationController.presentingViewController != nil
|
||||||
{
|
{
|
||||||
completionHandler(.failure(error))
|
self.navigationController.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
requestCertificate()
|
self.presentingViewController?.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -579,8 +615,6 @@ private extension AuthenticationOperation
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// We don't have private keys for any of the certificates,
|
|
||||||
// so we need to revoke one and create a new one.
|
|
||||||
replaceCertificate(from: certificates)
|
replaceCertificate(from: certificates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -593,7 +627,7 @@ private extension AuthenticationOperation
|
|||||||
|
|
||||||
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||||
{
|
{
|
||||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
|
guard let udid = fetch_udid()?.toString() else {
|
||||||
return completionHandler(.failure(OperationError.unknownUDID))
|
return completionHandler(.failure(OperationError.unknownUDID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import CoreData
|
|||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import EmotionalDamage
|
import EmotionalDamage
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
enum RefreshError: LocalizedError
|
typealias RefreshError = RefreshErrorCode.Error
|
||||||
|
enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
|
||||||
{
|
{
|
||||||
case noInstalledApps
|
case noInstalledApps
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorFailureReason: String {
|
||||||
switch self
|
switch self
|
||||||
{
|
{
|
||||||
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
||||||
@@ -93,11 +95,23 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
|
|||||||
super.main()
|
super.main()
|
||||||
|
|
||||||
guard !self.installedApps.isEmpty else {
|
guard !self.installedApps.isEmpty else {
|
||||||
self.finish(.failure(RefreshError.noInstalledApps))
|
self.finish(.failure(RefreshError(.noInstalledApps)))
|
||||||
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))
|
||||||
|
}
|
||||||
|
if #available(iOS 17, *) {
|
||||||
|
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
|
||||||
|
} else {
|
||||||
|
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,7 +203,7 @@ private extension BackgroundRefreshAppsOperation
|
|||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
var shouldPresentAlert = false
|
var shouldPresentAlert = true
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -205,20 +219,18 @@ private extension BackgroundRefreshAppsOperation
|
|||||||
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
||||||
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
||||||
}
|
}
|
||||||
catch RefreshError.noInstalledApps
|
catch ~OperationError.Code.noWiFi, ~RefreshErrorCode.noInstalledApps
|
||||||
{
|
{
|
||||||
shouldPresentAlert = false
|
shouldPresentAlert = false
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
print("Failed to refresh apps in background.", error)
|
print("Failed to refresh apps in background.", error)
|
||||||
|
|
||||||
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 = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldPresentAlert
|
if shouldPresentAlert
|
||||||
{
|
{
|
||||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ class BackupAppOperation: ResultOperation<Void>
|
|||||||
private var appName: String?
|
private var appName: String?
|
||||||
private var timeoutTimer: Timer?
|
private var timeoutTimer: Timer?
|
||||||
|
|
||||||
|
private weak var applicationWillReturnObserver: NSObjectProtocol?
|
||||||
|
private weak var backupResponseObserver: NSObjectProtocol?
|
||||||
|
|
||||||
init(action: Action, context: InstallAppOperationContext)
|
init(action: Action, context: InstallAppOperationContext)
|
||||||
{
|
{
|
||||||
self.action = action
|
self.action = action
|
||||||
@@ -43,10 +46,7 @@ class BackupAppOperation: ResultOperation<Void>
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let error = self.context.error
|
if let error = self.context.error { throw error }
|
||||||
{
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
|
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
|
||||||
context.perform {
|
context.perform {
|
||||||
@@ -55,13 +55,15 @@ class BackupAppOperation: ResultOperation<Void>
|
|||||||
let appName = installedApp.name
|
let appName = installedApp.name
|
||||||
self.appName = appName
|
self.appName = appName
|
||||||
|
|
||||||
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
|
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else {
|
||||||
|
throw OperationError.appNotFound(name: appName)
|
||||||
|
}
|
||||||
let altstoreOpenURL = altstoreApp.openAppURL
|
let altstoreOpenURL = altstoreApp.openAppURL
|
||||||
|
|
||||||
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
|
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
|
||||||
returnURLComponents?.host = "appBackupResponse"
|
returnURLComponents?.host = "appBackupResponse"
|
||||||
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
|
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
|
||||||
|
|
||||||
var openURLComponents = URLComponents()
|
var openURLComponents = URLComponents()
|
||||||
openURLComponents.scheme = installedApp.openAppURL.scheme
|
openURLComponents.scheme = installedApp.openAppURL.scheme
|
||||||
openURLComponents.host = self.action.rawValue
|
openURLComponents.host = self.action.rawValue
|
||||||
@@ -153,8 +155,11 @@ private extension BackupAppOperation
|
|||||||
{
|
{
|
||||||
func registerObservers()
|
func registerObservers()
|
||||||
{
|
{
|
||||||
var applicationWillReturnObserver: NSObjectProtocol!
|
self.applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
|
||||||
applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
|
defer {
|
||||||
|
self?.applicationWillReturnObserver.map { NotificationCenter.default.removeObserver($0) }
|
||||||
|
}
|
||||||
|
|
||||||
guard let self = self, !self.isFinished else { return }
|
guard let self = self, !self.isFinished else { return }
|
||||||
|
|
||||||
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
|
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
|
||||||
@@ -166,18 +171,17 @@ private extension BackupAppOperation
|
|||||||
self.finish(.failure(OperationError.timedOut))
|
self.finish(.failure(OperationError.timedOut))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var backupResponseObserver: NSObjectProtocol!
|
self.backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
|
||||||
backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
|
defer {
|
||||||
|
self?.backupResponseObserver.map { NotificationCenter.default.removeObserver($0) }
|
||||||
|
}
|
||||||
|
|
||||||
self?.timeoutTimer?.invalidate()
|
self?.timeoutTimer?.invalidate()
|
||||||
|
|
||||||
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
|
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
|
||||||
self?.finish(result)
|
self?.finish(result)
|
||||||
|
|
||||||
NotificationCenter.default.removeObserver(backupResponseObserver!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
208
AltStore/Operations/ClearAppCacheOperation.swift
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
//
|
||||||
|
// ClearAppCacheOperation.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 9/27/22.
|
||||||
|
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AltStoreCore
|
||||||
|
/*
|
||||||
|
struct BatchError: ALTLocalizedError
|
||||||
|
{
|
||||||
|
|
||||||
|
enum Code: Int, ALTErrorCode
|
||||||
|
{
|
||||||
|
typealias Error = BatchError
|
||||||
|
|
||||||
|
case batchError
|
||||||
|
}
|
||||||
|
|
||||||
|
var code: Code = .batchError
|
||||||
|
var underlyingErrors: [Error]
|
||||||
|
|
||||||
|
var errorTitle: String?
|
||||||
|
var errorFailure: String?
|
||||||
|
|
||||||
|
init(errors: [Error])
|
||||||
|
{
|
||||||
|
self.underlyingErrors = errors
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorFailureReason: String {
|
||||||
|
guard !self.underlyingErrors.isEmpty else { return NSLocalizedString("An unknown error occured.", comment: "") }
|
||||||
|
|
||||||
|
let errorMessages = self.underlyingErrors.map { $0.localizedDescription }
|
||||||
|
|
||||||
|
let message = errorMessages.joined(separator: "\n\n")
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
@objc(ClearAppCacheOperation)
|
||||||
|
class ClearAppCacheOperation: ResultOperation<Void>
|
||||||
|
{
|
||||||
|
private let coordinator = NSFileCoordinator()
|
||||||
|
private let coordinatorQueue = OperationQueue()
|
||||||
|
|
||||||
|
override init()
|
||||||
|
{
|
||||||
|
self.coordinatorQueue.name = "AltStore - ClearAppCacheOperation Queue"
|
||||||
|
}
|
||||||
|
|
||||||
|
override func main()
|
||||||
|
{
|
||||||
|
super.main()
|
||||||
|
|
||||||
|
var allErrors = [Error]()
|
||||||
|
|
||||||
|
self.clearTemporaryDirectory { result in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
|
||||||
|
case .failure(let error): allErrors.append(error)
|
||||||
|
case .success: break
|
||||||
|
}
|
||||||
|
|
||||||
|
self.removeUninstalledAppBackupDirectories { result in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
|
||||||
|
case .failure(let error): allErrors.append(error)
|
||||||
|
case .success: break
|
||||||
|
}
|
||||||
|
|
||||||
|
if allErrors.isEmpty
|
||||||
|
{
|
||||||
|
self.finish(.success(()))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.finish(.failure(OperationError.cacheClearError(errors: allErrors.map({ error in
|
||||||
|
return error.localizedDescription
|
||||||
|
}))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ClearAppCacheOperation
|
||||||
|
{
|
||||||
|
func clearTemporaryDirectory(completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let intent = NSFileAccessIntent.writingIntent(with: FileManager.default.temporaryDirectory, options: [.forDeleting])
|
||||||
|
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
|
||||||
|
includingPropertiesForKeys: [],
|
||||||
|
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
|
||||||
|
var errors = [Error]()
|
||||||
|
|
||||||
|
for fileURL in fileURLs
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
print("[ALTLog] Removing item from temporary directory:", fileURL.lastPathComponent)
|
||||||
|
try FileManager.default.removeItem(at: fileURL)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("[ALTLog] Failed to remove \(fileURL.lastPathComponent) from temporary directory.", error)
|
||||||
|
errors.append(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.isEmpty
|
||||||
|
{
|
||||||
|
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
|
||||||
|
return error.localizedDescription
|
||||||
|
}))))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeUninstalledAppBackupDirectories(completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
guard let backupsDirectory = FileManager.default.appBackupsDirectory else { return completion(.failure(OperationError.missingAppGroup)) }
|
||||||
|
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||||
|
let installedAppBundleIDs = Set(InstalledApp.all(in: context).map { $0.bundleIdentifier })
|
||||||
|
|
||||||
|
let intent = NSFileAccessIntent.writingIntent(with: backupsDirectory, options: [.forDeleting])
|
||||||
|
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
guard FileManager.default.fileExists(atPath: intent.url.path, isDirectory: &isDirectory), isDirectory.boolValue else {
|
||||||
|
completion(.success(()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
|
||||||
|
includingPropertiesForKeys: [.isDirectoryKey, .nameKey],
|
||||||
|
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
|
||||||
|
var errors = [Error]()
|
||||||
|
|
||||||
|
|
||||||
|
for backupDirectory in fileURLs
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let resourceValues = try backupDirectory.resourceValues(forKeys: [.isDirectoryKey, .nameKey])
|
||||||
|
guard let isDirectory = resourceValues.isDirectory, let bundleID = resourceValues.name else { continue }
|
||||||
|
|
||||||
|
if isDirectory && !installedAppBundleIDs.contains(bundleID) && !AppManager.shared.isActivelyManagingApp(withBundleID: bundleID)
|
||||||
|
{
|
||||||
|
print("[ALTLog] Removing backup directory for uninstalled app:", bundleID)
|
||||||
|
try FileManager.default.removeItem(at: backupDirectory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("[ALTLog] Failed to remove app backup directory:", error)
|
||||||
|
errors.append(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.isEmpty
|
||||||
|
{
|
||||||
|
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
|
||||||
|
return error.localizedDescription
|
||||||
|
}))))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("[ALTLog] Failed to remove app backup directory:", error)
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,11 +31,7 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
|
|||||||
{
|
{
|
||||||
super.main()
|
super.main()
|
||||||
|
|
||||||
if let error = self.context.error
|
if let error = self.context.error { return self.finish(.failure(error)) }
|
||||||
{
|
|
||||||
self.finish(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
|
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
|
||||||
@@ -44,20 +40,15 @@ final 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.progress.completedUnitCount += 1
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
installedApp.isActive = false
|
||||||
}
|
self.finish(.success(installedApp))
|
||||||
} catch Uhoh.Bad(let code) {
|
break
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
} catch {
|
} catch {
|
||||||
self.finish(.failure(ALTServerError(.unknownResponse)))
|
self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.progress.completedUnitCount += 1
|
|
||||||
installedApp.isActive = false
|
|
||||||
self.finish(.success(installedApp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,66 +12,108 @@ import Roxas
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
private extension DownloadAppOperation
|
|
||||||
{
|
|
||||||
struct DependencyError: ALTLocalizedError
|
|
||||||
{
|
|
||||||
let dependency: Dependency
|
|
||||||
let error: Error
|
|
||||||
|
|
||||||
var failure: String? {
|
|
||||||
return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
var underlyingError: Error? {
|
|
||||||
return self.error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc(DownloadAppOperation)
|
@objc(DownloadAppOperation)
|
||||||
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||||
{
|
{
|
||||||
let app: AppProtocol
|
let app: AppProtocol
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
|
|
||||||
|
private let appName: String
|
||||||
private let bundleIdentifier: String
|
private let bundleIdentifier: String
|
||||||
private var sourceURL: URL?
|
|
||||||
private let destinationURL: URL
|
private let destinationURL: URL
|
||||||
|
|
||||||
private let session = URLSession(configuration: .default)
|
private let session = URLSession(configuration: .default)
|
||||||
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||||
|
|
||||||
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
||||||
{
|
{
|
||||||
self.app = app
|
self.app = app
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
|
self.appName = app.name
|
||||||
self.bundleIdentifier = app.bundleIdentifier
|
self.bundleIdentifier = app.bundleIdentifier
|
||||||
self.sourceURL = app.url
|
|
||||||
self.destinationURL = destinationURL
|
self.destinationURL = destinationURL
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// App = 3, Dependencies = 1
|
// App = 3, Dependencies = 1
|
||||||
self.progress.totalUnitCount = 4
|
self.progress.totalUnitCount = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
override func main()
|
override func main()
|
||||||
{
|
{
|
||||||
super.main()
|
super.main()
|
||||||
|
|
||||||
if let error = self.context.error
|
if let error = self.context.error
|
||||||
{
|
{
|
||||||
self.finish(.failure(error))
|
self.finish(.failure(error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Downloading App:", self.bundleIdentifier)
|
print("Downloading App:", self.bundleIdentifier)
|
||||||
|
|
||||||
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
|
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
|
||||||
|
|
||||||
self.downloadApp(from: sourceURL) { result in
|
guard let storeApp = self.app as? StoreApp else { return self.download(self.app) }
|
||||||
|
storeApp.managedObjectContext?.perform {
|
||||||
|
do {
|
||||||
|
let latestVersion = try self.verify(storeApp)
|
||||||
|
self.download(latestVersion)
|
||||||
|
} catch let error as VerificationError where error.code == .iOSVersionNotSupported {
|
||||||
|
guard let presentingViewController = self.context.presentingViewController,
|
||||||
|
let latestSupportedVersion = storeApp.latestSupportedVersion,
|
||||||
|
case let version = latestSupportedVersion.version,
|
||||||
|
version != storeApp.installedApp?.version else {
|
||||||
|
return self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
|
||||||
|
let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "")
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
|
||||||
|
self.finish(.failure(OperationError.cancelled))
|
||||||
|
})
|
||||||
|
alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("Download %@ %@", comment: ""), self.appName, version), style: .default) { _ in
|
||||||
|
self.download(latestSupportedVersion)
|
||||||
|
})
|
||||||
|
presentingViewController.present(alertController, animated: true)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func finish(_ result: Result<ALTApplication, any Error>) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
||||||
|
} catch {
|
||||||
|
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
||||||
|
}
|
||||||
|
super.finish(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DownloadAppOperation {
|
||||||
|
func verify(_ storeApp: StoreApp) throws -> AppVersion {
|
||||||
|
guard let version = storeApp.latestAvailableVersion else {
|
||||||
|
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
|
||||||
|
throw OperationError.unknown(failureReason: failureReason)
|
||||||
|
}
|
||||||
|
if let minOSVersion = version.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
|
||||||
|
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: minOSVersion)
|
||||||
|
} else if let maxOSVersion = version.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
|
||||||
|
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: maxOSVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
func download(@Managed _ app: AppProtocol) {
|
||||||
|
guard let sourceURL = $app.url else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
|
||||||
|
|
||||||
|
self.downloadIPA(from: sourceURL) { result in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let application = try result.get()
|
let application = try result.get()
|
||||||
@@ -112,24 +154,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func finish(_ result: Result<ALTApplication, Error>)
|
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.finish(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension DownloadAppOperation
|
|
||||||
{
|
|
||||||
func downloadApp(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
|
||||||
{
|
{
|
||||||
func finishOperation(_ result: Result<URL, Error>)
|
func finishOperation(_ result: Result<URL, Error>)
|
||||||
{
|
{
|
||||||
@@ -138,8 +163,8 @@ private extension DownloadAppOperation
|
|||||||
let fileURL = try result.get()
|
let fileURL = try result.get()
|
||||||
|
|
||||||
var isDirectory: ObjCBool = false
|
var isDirectory: ObjCBool = false
|
||||||
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
|
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
|
||||||
|
|
||||||
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
let appBundleURL: URL
|
let appBundleURL: URL
|
||||||
@@ -178,6 +203,9 @@ private extension DownloadAppOperation
|
|||||||
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
|
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
|
if let response = response as? HTTPURLResponse {
|
||||||
|
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: sourceURL]) }
|
||||||
|
}
|
||||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
finishOperation(.success(fileURL))
|
finishOperation(.success(fileURL))
|
||||||
|
|
||||||
@@ -252,7 +280,7 @@ private extension DownloadAppOperation
|
|||||||
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
|
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
|
||||||
|
|
||||||
var dependencyURLs = Set<URL>()
|
var dependencyURLs = Set<URL>()
|
||||||
var dependencyError: DependencyError?
|
var dependencyError: Error?
|
||||||
|
|
||||||
let dispatchGroup = DispatchGroup()
|
let dispatchGroup = DispatchGroup()
|
||||||
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
|
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
|
||||||
@@ -285,7 +313,7 @@ private extension DownloadAppOperation
|
|||||||
}
|
}
|
||||||
catch let error as DecodingError
|
catch let error as DecodingError
|
||||||
{
|
{
|
||||||
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name))
|
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not determine dependencies for %@.", comment: ""), application.name))
|
||||||
completionHandler(.failure(nsError))
|
completionHandler(.failure(nsError))
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -294,7 +322,7 @@ private extension DownloadAppOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void)
|
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||||
{
|
{
|
||||||
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
|
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
|
||||||
do
|
do
|
||||||
@@ -315,9 +343,10 @@ private extension DownloadAppOperation
|
|||||||
|
|
||||||
completionHandler(.success(destinationURL))
|
completionHandler(.success(destinationURL))
|
||||||
}
|
}
|
||||||
catch
|
catch let error as NSError
|
||||||
{
|
{
|
||||||
completionHandler(.failure(DependencyError(dependency: dependency, error: error)))
|
let localizedFailure = String(format: NSLocalizedString("The dependency '%@' could not be downloaded.", comment: ""), dependency.preferredFilename)
|
||||||
|
completionHandler(.failure(error.withLocalizedFailure(localizedFailure)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
||||||
|
|||||||
@@ -9,9 +9,17 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import minimuxer
|
import minimuxer
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
|
enum SideJITServerErrorType: Error {
|
||||||
|
case invalidURL
|
||||||
|
case errorConnecting
|
||||||
|
case deviceNotFound
|
||||||
|
case other(String)
|
||||||
|
}
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
protocol EnableJITContext
|
protocol EnableJITContext
|
||||||
{
|
{
|
||||||
@@ -43,25 +51,105 @@ final 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)) }
|
||||||
|
if #available(iOS 17, *) {
|
||||||
installedApp.managedObjectContext?.perform {
|
let sideJITenabled = UserDefaults.standard.sidejitenable
|
||||||
let v = minimuxer_to_operation(code: 1)
|
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||||
|
|
||||||
do {
|
if sideJITenabled {
|
||||||
var x = try debug_app(app_id: installedApp.resignedBundleIdentifier)
|
installedApp.managedObjectContext?.perform {
|
||||||
switch x {
|
EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
|
||||||
case .Good:
|
switch result {
|
||||||
self.finish(.success(()))
|
case .failure(let error):
|
||||||
case .Bad(let code):
|
switch error {
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
case .invalidURL, .errorConnecting:
|
||||||
|
self.finish(.failure(OperationError.unableToConnectSideJIT))
|
||||||
|
case .deviceNotFound:
|
||||||
|
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
|
||||||
|
case .other(let message):
|
||||||
|
if let startRange = message.range(of: "<p>"),
|
||||||
|
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
|
||||||
|
let pContent = message[startRange.upperBound..<endRange.lowerBound]
|
||||||
|
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
|
||||||
|
print(message + " + " + String(pContent))
|
||||||
|
} else {
|
||||||
|
print(message)
|
||||||
|
self.finish(.failure(OperationError.SideJITIssue(error: message)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .success():
|
||||||
|
self.finish(.success(()))
|
||||||
|
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
installedApp.managedObjectContext?.perform {
|
||||||
|
var retries = 3
|
||||||
|
while (retries > 0){
|
||||||
|
do {
|
||||||
|
try debug_app(installedApp.resignedBundleIdentifier)
|
||||||
|
self.finish(.success(()))
|
||||||
|
retries = 0
|
||||||
|
} catch {
|
||||||
|
retries -= 1
|
||||||
|
if (retries <= 0){
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch Uhoh.Bad(let code) {
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
} catch {
|
|
||||||
self.finish(.failure(OperationError.unknown))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 17, *)
|
||||||
|
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
|
||||||
|
guard let udid = fetch_udid()?.toString() else {
|
||||||
|
completion(.failure(.other("Unable to get UDID")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var SJSURL = serverurl
|
||||||
|
|
||||||
|
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||||
|
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !SJSURL.hasPrefix("http") {
|
||||||
|
completion(.failure(.invalidURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
|
||||||
|
|
||||||
|
let url = URL(string: fullurl)!
|
||||||
|
|
||||||
|
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
|
||||||
|
if let error = error {
|
||||||
|
completion(.failure(.errorConnecting))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
|
||||||
|
|
||||||
|
if datastring == "Enabled JIT for '\(installedapp.name)'!" {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "JIT Successfully Enabled"
|
||||||
|
content.subtitle = "JIT Enabled For \(installedapp.name)"
|
||||||
|
content.sound = UNNotificationSound.default
|
||||||
|
|
||||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
|
||||||
|
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
completion(.success(()))
|
||||||
|
} else {
|
||||||
|
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
|
||||||
|
completion(.failure(errorType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
final 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,32 +45,413 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = AnisetteManager.currentURL
|
self.url = URL(string: UserDefaults.standard.menuAnisetteURL)
|
||||||
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
|
||||||
DLOG("Anisette used: %@", formattedJSON)
|
// convert data to json
|
||||||
self.finish(.success(anisette))
|
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: WebSocketClient) {
|
||||||
|
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")
|
||||||
|
let url = UserDefaults.standard.menuAnisetteURL
|
||||||
|
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 WebSocketClient {
|
||||||
|
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) } }
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
|
|||||||
let session = self.context.session
|
let session = self.context.session
|
||||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
|
||||||
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
|
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
|
||||||
|
|
||||||
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
|
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
|
||||||
|
|
||||||
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
|
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
|
||||||
@@ -260,16 +260,21 @@ extension FetchProvisioningProfilesOperation
|
|||||||
{
|
{
|
||||||
if let expirationDate = sortedExpirationDates.first
|
if let expirationDate = sortedExpirationDates.first
|
||||||
{
|
{
|
||||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw ALTAppleAPIError(.maximumAppIDLimitReached)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//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
|
||||||
@@ -281,7 +286,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
{
|
{
|
||||||
if let expirationDate = sortedExpirationDates.first
|
if let expirationDate = sortedExpirationDates.first
|
||||||
{
|
{
|
||||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -384,19 +389,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 +503,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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Network
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
@objc(InstallAppOperation)
|
@objc(InstallAppOperation)
|
||||||
final class InstallAppOperation: ResultOperation<InstalledApp>
|
final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||||
@@ -40,12 +41,14 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
guard
|
guard
|
||||||
let certificate = self.context.certificate,
|
let certificate = self.context.certificate,
|
||||||
let resignedApp = self.context.resignedApp
|
let resignedApp = self.context.resignedApp,
|
||||||
|
let provisioningProfiles = self.context.provisioningProfiles
|
||||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
|
||||||
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
backgroundContext.perform {
|
backgroundContext.perform {
|
||||||
|
|
||||||
|
|
||||||
/* App */
|
/* App */
|
||||||
let installedApp: InstalledApp
|
let installedApp: InstalledApp
|
||||||
|
|
||||||
@@ -115,8 +118,7 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
|
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
|
||||||
self.cleanUp()
|
self.cleanUp()
|
||||||
|
|
||||||
var activeProfiles: Set<String>?
|
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit, provisioningProfiles.contains(where: { $1.isFreeProvisioningProfile == true })
|
||||||
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
|
|
||||||
{
|
{
|
||||||
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
|
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
|
||||||
|
|
||||||
@@ -141,23 +143,70 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
installedApp.isActive = false
|
installedApp.isActive = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
|
else
|
||||||
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
{
|
||||||
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
installedApp.isActive = true
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ns_bundle = NSString(string: installedApp.bundleIdentifier)
|
var installing = true
|
||||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
if installedApp.storeApp?.bundleIdentifier.range(of: Bundle.Info.appbundleIdentifier) != nil {
|
||||||
|
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
|
if UIApplication.shared.applicationState != .active {
|
||||||
|
print("We are not in the foreground, let's not do anything")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !installing {
|
||||||
|
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 = "SideStore will automatically move to the homescreen to finish refreshing!"
|
||||||
|
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
|
||||||
|
UNUserNotificationCenter.current().add(notification)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
print("Notifications are not enabled")
|
||||||
|
|
||||||
|
let alert = UIAlertController(title: "Finish Refresh", message: "Please reopen SideStore after the process is finished.To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen manually or by hitting Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
|
||||||
|
print("Going home")
|
||||||
|
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||||
|
}))
|
||||||
|
|
||||||
|
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 go home")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let res = minimuxer_install_ipa(ns_bundle_ptr)
|
do {
|
||||||
if res == 0 {
|
try install_ipa(installedApp.bundleIdentifier)
|
||||||
|
installing = false
|
||||||
installedApp.refreshedDate = Date()
|
installedApp.refreshedDate = Date()
|
||||||
self.finish(.success(installedApp))
|
self.finish(.success(installedApp))
|
||||||
|
} catch let error {
|
||||||
} else {
|
installing = false
|
||||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,10 +223,11 @@ final 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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import Roxas
|
|||||||
class ResultOperation<ResultType>: Operation
|
class ResultOperation<ResultType>: Operation
|
||||||
{
|
{
|
||||||
var resultHandler: ((Result<ResultType, Error>) -> Void)?
|
var resultHandler: ((Result<ResultType, Error>) -> Void)?
|
||||||
|
|
||||||
|
// Should only be set by subclasses
|
||||||
|
var localizedFailure: String?
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
override func finish()
|
override func finish()
|
||||||
{
|
{
|
||||||
@@ -22,16 +25,20 @@ class ResultOperation<ResultType>: Operation
|
|||||||
func finish(_ result: Result<ResultType, Error>)
|
func finish(_ result: Result<ResultType, Error>)
|
||||||
{
|
{
|
||||||
guard !self.isFinished else { return }
|
guard !self.isFinished else { return }
|
||||||
|
|
||||||
|
var result = result
|
||||||
|
|
||||||
if self.isCancelled
|
if self.isCancelled
|
||||||
{
|
{
|
||||||
self.resultHandler?(.failure(OperationError.cancelled))
|
result = .failure(OperationError.cancelled)
|
||||||
}
|
}
|
||||||
else
|
else if case .failure(let nsError as NSError) = result, let localizedFailure, nsError.localizedFailure == nil {
|
||||||
{
|
// Error doesn't have its own localizedFailure, so we give it the Operation's (if it exists)
|
||||||
self.resultHandler?(result)
|
let error = nsError.withLocalizedFailure(localizedFailure)
|
||||||
|
result = .failure(error)
|
||||||
}
|
}
|
||||||
|
self.resultHandler?(result)
|
||||||
|
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,81 +8,186 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AltSign
|
import AltSign
|
||||||
|
import AltStoreCore
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
enum OperationError: LocalizedError
|
extension OperationError
|
||||||
{
|
{
|
||||||
static let domain = OperationError.unknown._domain
|
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||||
|
typealias Error = OperationError
|
||||||
|
|
||||||
|
// General
|
||||||
|
case unknown = 1000
|
||||||
|
case unknownResult
|
||||||
|
case cancelled
|
||||||
|
case timedOut
|
||||||
|
case unableToConnectSideJIT
|
||||||
|
case unableToRespondSideJITDevice
|
||||||
|
case wrongSideJITIP
|
||||||
|
case SideJITIssue // (error: String)
|
||||||
|
case refreshsidejit
|
||||||
|
case notAuthenticated
|
||||||
|
case appNotFound
|
||||||
|
case unknownUDID
|
||||||
|
case invalidApp
|
||||||
|
case invalidParameters
|
||||||
|
case maximumAppIDLimitReached//((application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
|
||||||
|
case noSources
|
||||||
|
case openAppFailed//(name: String)
|
||||||
|
case missingAppGroup
|
||||||
|
|
||||||
|
// Connection
|
||||||
|
case noWiFi = 1200
|
||||||
|
case tooNewError
|
||||||
|
case anisetteV1Error//(message: String)
|
||||||
|
case provisioningError//(result: String, message: String?)
|
||||||
|
case anisetteV3Error//(message: String)
|
||||||
|
|
||||||
|
case cacheClearError//(errors: [String])
|
||||||
|
}
|
||||||
|
|
||||||
|
static let unknownResult: OperationError = .init(code: .unknownResult)
|
||||||
|
static let cancelled: OperationError = .init(code: .cancelled)
|
||||||
|
static let timedOut: OperationError = .init(code: .timedOut)
|
||||||
|
static let unableToConnectSideJIT: OperationError = .init(code: .unableToConnectSideJIT)
|
||||||
|
static let unableToRespondSideJITDevice: OperationError = .init(code: .unableToRespondSideJITDevice)
|
||||||
|
static let wrongSideJITIP: OperationError = .init(code: .wrongSideJITIP)
|
||||||
|
static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
|
||||||
|
static let unknownUDID: OperationError = .init(code: .unknownUDID)
|
||||||
|
static let invalidApp: OperationError = .init(code: .invalidApp)
|
||||||
|
static let invalidParameters: OperationError = .init(code: .invalidParameters)
|
||||||
|
static let noSources: OperationError = .init(code: .noSources)
|
||||||
|
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
|
||||||
|
|
||||||
|
static let noWiFi: OperationError = .init(code: .noWiFi)
|
||||||
|
static let tooNewError: OperationError = .init(code: .tooNewError)
|
||||||
|
static let provisioningError: OperationError = .init(code: .provisioningError)
|
||||||
|
static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error)
|
||||||
|
static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error)
|
||||||
|
|
||||||
|
static let cacheClearError: OperationError = .init(code: .cacheClearError)
|
||||||
|
|
||||||
|
static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||||
|
OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func appNotFound(name: String?) -> OperationError {
|
||||||
|
OperationError(code: .appNotFound, appName: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func openAppFailed(name: String?) -> OperationError {
|
||||||
|
OperationError(code: .openAppFailed, appName: name)
|
||||||
|
}
|
||||||
|
|
||||||
case unknown
|
static func SideJITIssue(error: String?) -> OperationError {
|
||||||
case unknownResult
|
var o = OperationError(code: .SideJITIssue)
|
||||||
case cancelled
|
o.errorFailure = error
|
||||||
case timedOut
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
case notAuthenticated
|
static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
|
||||||
case appNotFound
|
OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
||||||
|
}
|
||||||
case unknownUDID
|
|
||||||
|
static func provisioningError(result: String, message: String?) -> OperationError {
|
||||||
case invalidApp
|
var o = OperationError(code: .provisioningError, failureReason: result)
|
||||||
case invalidParameters
|
o.errorTitle = message
|
||||||
|
return o
|
||||||
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
|
}
|
||||||
|
|
||||||
case noSources
|
static func cacheClearError(errors: [String]) -> OperationError {
|
||||||
|
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
|
||||||
case openAppFailed(name: String)
|
}
|
||||||
case missingAppGroup
|
|
||||||
|
static func anisetteV1Error(message: String) -> OperationError {
|
||||||
case noDevice
|
OperationError(code: .anisetteV1Error, failureReason: message)
|
||||||
case createService(name: String)
|
}
|
||||||
case getFromDevice(name: String)
|
|
||||||
case setArgument(name: String)
|
static func anisetteV3Error(message: String) -> OperationError {
|
||||||
case afc
|
OperationError(code: .anisetteV3Error, failureReason: message)
|
||||||
case install
|
}
|
||||||
case uninstall
|
|
||||||
case lookupApps
|
}
|
||||||
case detach
|
|
||||||
case functionArguments
|
|
||||||
case profileInstall
|
struct OperationError: ALTLocalizedError {
|
||||||
case noConnection
|
|
||||||
|
let code: Code
|
||||||
var failureReason: String? {
|
|
||||||
switch self {
|
var errorTitle: String?
|
||||||
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
|
var errorFailure: String?
|
||||||
|
|
||||||
|
var appName: String?
|
||||||
|
|
||||||
|
var requiredAppIDs: Int?
|
||||||
|
var availableAppIDs: Int?
|
||||||
|
var expirationDate: Date?
|
||||||
|
|
||||||
|
var sourceFile: String?
|
||||||
|
var sourceLine: UInt?
|
||||||
|
|
||||||
|
private var _failureReason: String?
|
||||||
|
|
||||||
|
private init(code: Code, failureReason: String? = nil,
|
||||||
|
appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil,
|
||||||
|
expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){
|
||||||
|
self.code = code
|
||||||
|
self._failureReason = failureReason
|
||||||
|
|
||||||
|
self.appName = appName
|
||||||
|
self.requiredAppIDs = requiredAppIDs
|
||||||
|
self.availableAppIDs = availableAppIDs
|
||||||
|
self.expirationDate = expirationDate
|
||||||
|
self.sourceFile = sourceFile
|
||||||
|
self.sourceLine = sourceLine
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorFailureReason: String {
|
||||||
|
switch self.code {
|
||||||
|
case .unknown:
|
||||||
|
var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occurred.", comment: "")
|
||||||
|
guard let sourceFile, let sourceLine else { return failureReason }
|
||||||
|
failureReason += " (\(sourceFile) line \(sourceLine)"
|
||||||
|
return failureReason
|
||||||
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
||||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||||
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
|
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
|
||||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||||
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
|
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "")
|
||||||
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
|
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
|
||||||
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
|
|
||||||
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
|
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
|
||||||
|
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
|
||||||
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
|
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
|
||||||
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 accessed.", comment: "")
|
||||||
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
|
case .appNotFound:
|
||||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
|
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
||||||
case .noDevice: return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
|
||||||
case .createService(let name): return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
|
case .openAppFailed:
|
||||||
case .getFromDevice(let name): return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
|
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
||||||
case .setArgument(let name): return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
|
return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName)
|
||||||
case .afc: return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
|
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "")
|
||||||
case .install: return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
|
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it without SideJITServer at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "")
|
||||||
case .uninstall: return NSLocalizedString("Unable to uninstall the app", comment: "")
|
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "")
|
||||||
case .lookupApps: return NSLocalizedString("Unable to fetch apps from the device", comment: "")
|
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "")
|
||||||
case .detach: return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
|
||||||
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
|
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
|
||||||
case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
|
||||||
case .noConnection: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
|
case .provisioningError: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
|
||||||
|
case .anisetteV3Error: return 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: "")
|
||||||
|
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
|
||||||
|
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var recoverySuggestion: String? {
|
var recoverySuggestion: String? {
|
||||||
switch self
|
switch self.code
|
||||||
{
|
{
|
||||||
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
|
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
|
||||||
|
case .maximumAppIDLimitReached:
|
||||||
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
||||||
let message: String
|
guard let appName, let requiredAppIDs, let availableAppIDs, let expirationDate else { return baseMessage }
|
||||||
|
var message: String
|
||||||
|
|
||||||
if requiredAppIDs > 1
|
if requiredAppIDs > 1
|
||||||
{
|
{
|
||||||
let availableText: String
|
let availableText: String
|
||||||
@@ -94,23 +199,23 @@ enum OperationError: LocalizedError
|
|||||||
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
|
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
|
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
|
||||||
message = prefixMessage + " " + baseMessage
|
message = prefixMessage + " " + baseMessage + "\n\n"
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
|
message = baseMessage + " "
|
||||||
|
|
||||||
let dateComponentsFormatter = DateComponentsFormatter()
|
|
||||||
dateComponentsFormatter.maximumUnitCount = 1
|
|
||||||
dateComponentsFormatter.unitsStyle = .full
|
|
||||||
|
|
||||||
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
|
|
||||||
|
|
||||||
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
|
|
||||||
message = baseMessage + " " + remainingTimeMessage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: expirationDate)
|
||||||
|
let dateFormatter = DateComponentsFormatter()
|
||||||
|
dateFormatter.maximumUnitCount = 1
|
||||||
|
dateFormatter.unitsStyle = .full
|
||||||
|
|
||||||
|
let remainingTime = dateFormatter.string(from: dateComponents)!
|
||||||
|
|
||||||
|
message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
default: return nil
|
default: return nil
|
||||||
@@ -118,49 +223,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. This could mean an invalid pairing.", 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. This usually means an invalid pairing.", comment: "")
|
||||||
return OperationError.profileInstall
|
case .InstallApp(let message):
|
||||||
case 19:
|
return NSLocalizedString("Unable to install the app: \(message.toString())", 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,22 +25,38 @@ protocol PatchAppContext
|
|||||||
var error: Error? { get }
|
var error: Error? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PatchAppError: LocalizedError
|
extension PatchAppError
|
||||||
{
|
{
|
||||||
case unsupportedOperatingSystemVersion(OperatingSystemVersion)
|
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||||
|
typealias Error = PatchAppError
|
||||||
var errorDescription: String? {
|
|
||||||
switch self
|
case unsupportedOperatingSystemVersion
|
||||||
{
|
}
|
||||||
case .unsupportedOperatingSystemVersion(let osVersion):
|
|
||||||
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
|
static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError {
|
||||||
if osVersion.patchVersion != 0
|
PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion)
|
||||||
{
|
}
|
||||||
osVersionString += ".\(osVersion.patchVersion)"
|
}
|
||||||
|
|
||||||
|
struct PatchAppError: ALTLocalizedError {
|
||||||
|
let code: Code
|
||||||
|
|
||||||
|
var errorTitle: String?
|
||||||
|
var errorFailure: String?
|
||||||
|
|
||||||
|
var osVersion: OperatingSystemVersion?
|
||||||
|
|
||||||
|
var errorFailureReason: String {
|
||||||
|
switch self.code {
|
||||||
|
case .unsupportedOperatingSystemVersion:
|
||||||
|
let osVersionString: String
|
||||||
|
|
||||||
|
if let osVersion = self.osVersion?.stringValue {
|
||||||
|
osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion
|
||||||
|
} else {
|
||||||
|
osVersionString = NSLocalizedString("your device's iOS version", comment: "")
|
||||||
}
|
}
|
||||||
|
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
|
||||||
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
|
|
||||||
return errorDescription
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ private extension PatchViewController
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
|
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown() }
|
||||||
_ = try result.get()
|
_ = try result.get()
|
||||||
|
|
||||||
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
|
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
|
||||||
|
|||||||
@@ -35,34 +35,27 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let error = self.context.error
|
if let error = self.context.error { return self.finish(.failure(error)) }
|
||||||
{
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let profiles = self.context.provisioningProfiles else { throw OperationError.invalidParameters }
|
guard let profiles = self.context.provisioningProfiles else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
guard let app = self.context.app else { return self.finish(.failure(OperationError(.appNotFound(name: nil)))) }
|
||||||
guard let app = self.context.app else { throw OperationError.appNotFound }
|
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
|
||||||
print("Sending refresh app request...")
|
|
||||||
|
|
||||||
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 {
|
||||||
}
|
self.finish(.failure(MinimuxerError.ProfileInstall))
|
||||||
} catch Uhoh.Bad(let code) {
|
}
|
||||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
|
||||||
} catch {
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
self.finish(.failure(OperationError.unknown))
|
|
||||||
}
|
|
||||||
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)
|
||||||
self.managedObjectContext.perform {
|
self.managedObjectContext.perform {
|
||||||
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
|
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
|
||||||
|
self.finish(.failure(OperationError(.appNotFound(name: app.name))))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
installedApp.update(provisioningProfile: p.value)
|
installedApp.update(provisioningProfile: p.value)
|
||||||
@@ -75,9 +68,5 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
{
|
|
||||||
self.finish(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,15 +39,11 @@ final 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
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Roxas
|
|||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
@objc(ResignAppOperation)
|
@objc(ResignAppOperation)
|
||||||
final class ResignAppOperation: ResultOperation<ALTApplication>
|
final class ResignAppOperation: ResultOperation<ALTApplication>
|
||||||
@@ -61,6 +62,7 @@ final 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 }
|
||||||
@@ -114,7 +116,9 @@ private extension ResignAppOperation
|
|||||||
|
|
||||||
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
||||||
infoDictionary[Bundle.Info.altBundleID] = identifier
|
infoDictionary[Bundle.Info.altBundleID] = identifier
|
||||||
infoDictionary[Bundle.Info.devicePairingString] = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String
|
infoDictionary[Bundle.Info.devicePairingString] = "<insert pairing file here>"
|
||||||
|
infoDictionary.removeValue(forKey: "DTXcode")
|
||||||
|
infoDictionary.removeValue(forKey: "DTXcodeBuild")
|
||||||
|
|
||||||
for (key, value) in additionalInfoDictionaryValues
|
for (key, value) in additionalInfoDictionaryValues
|
||||||
{
|
{
|
||||||
@@ -147,6 +151,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 {
|
||||||
@@ -172,9 +184,9 @@ private extension ResignAppOperation
|
|||||||
|
|
||||||
if app.isAltStoreApp
|
if app.isAltStoreApp
|
||||||
{
|
{
|
||||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
guard let udid = fetch_udid()?.toString() as? String else { throw OperationError.unknownUDID }
|
||||||
guard let pairingFileString = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) as? String else { throw OperationError.unknownUDID }
|
guard let pairingFileString = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) as? String else { throw OperationError.unknownUDID }
|
||||||
additionalValues[Bundle.Info.devicePairingString] = pairingFileString
|
additionalValues[Bundle.Info.devicePairingString] = "<insert pairing file here>"
|
||||||
additionalValues[Bundle.Info.deviceID] = udid
|
additionalValues[Bundle.Info.deviceID] = udid
|
||||||
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
|
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
|
||||||
|
|
||||||
@@ -193,7 +205,7 @@ private extension ResignAppOperation
|
|||||||
// The embedded certificate + certificate identifier are already in app bundle, no need to update them.
|
// The embedded certificate + certificate identifier are already in app bundle, no need to update them.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if infoDictionary.keys.contains(Bundle.Info.deviceID), let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String
|
else if infoDictionary.keys.contains(Bundle.Info.deviceID), let udid = fetch_udid()?.toString() as? String
|
||||||
{
|
{
|
||||||
// There is an ALTDeviceID entry, so assume the app is using AltKit and replace it with the device's UDID.
|
// There is an ALTDeviceID entry, so assume the app is using AltKit and replace it with the device's UDID.
|
||||||
additionalValues[Bundle.Info.deviceID] = udid
|
additionalValues[Bundle.Info.deviceID] = udid
|
||||||
@@ -218,6 +230,7 @@ private extension ResignAppOperation
|
|||||||
|
|
||||||
// Prepare app
|
// Prepare app
|
||||||
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
||||||
|
try self.removeMissingAppExtensionReferences(from: appBundle)
|
||||||
|
|
||||||
if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
||||||
{
|
{
|
||||||
@@ -258,4 +271,28 @@ private extension ResignAppOperation
|
|||||||
|
|
||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeMissingAppExtensionReferences(from bundle: Bundle) throws
|
||||||
|
{
|
||||||
|
// If app extensions have been removed from an app (either by AltStore or the developer),
|
||||||
|
// we must remove all references to them from SC_Info/Manifest.plist (if it exists).
|
||||||
|
|
||||||
|
let scInfoURL = bundle.bundleURL.appendingPathComponent("SC_Info")
|
||||||
|
let manifestPlistURL = scInfoURL.appendingPathComponent("Manifest.plist")
|
||||||
|
|
||||||
|
guard let manifestPlist = NSMutableDictionary(contentsOf: manifestPlistURL), let sinfReplicationPaths = manifestPlist["SinfReplicationPaths"] as? [String] else { return }
|
||||||
|
|
||||||
|
// Remove references to missing files.
|
||||||
|
let filteredReplicationPaths = sinfReplicationPaths.filter { path in
|
||||||
|
guard let fileURL = URL(string: path, relativeTo: bundle.bundleURL) else { return false }
|
||||||
|
|
||||||
|
let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
|
||||||
|
return fileExists
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestPlist["SinfReplicationPaths"] = filteredReplicationPaths
|
||||||
|
|
||||||
|
// Save updated Manifest.plist to disk.
|
||||||
|
try manifestPlist.write(to: manifestPlistURL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Foundation
|
|||||||
import Network
|
import Network
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
import minimuxer
|
||||||
|
|
||||||
@objc(SendAppOperation)
|
@objc(SendAppOperation)
|
||||||
final class SendAppOperation: ResultOperation<()>
|
final class SendAppOperation: ResultOperation<()>
|
||||||
@@ -32,8 +33,7 @@ final class SendAppOperation: ResultOperation<()>
|
|||||||
|
|
||||||
if let error = self.context.error
|
if let error = self.context.error
|
||||||
{
|
{
|
||||||
self.finish(.failure(error))
|
return self.finish(.failure(error))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) }
|
||||||
@@ -44,25 +44,20 @@ final class SendAppOperation: ResultOperation<()>
|
|||||||
|
|
||||||
print("AFC App `fileURL`: \(fileURL.absoluteString)")
|
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())
|
||||||
}
|
self.progress.completedUnitCount += 1
|
||||||
let res = minimuxer_yeet_app_afc(ns_bundle_ptr, pls, UInt(data.length))
|
self.finish(.success(()))
|
||||||
if res == 0 {
|
} catch {
|
||||||
print("minimuxer_yeet_app_afc `res` == \(res)")
|
self.finish(.failure(MinimuxerError.RwAfc))
|
||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
self.finish(.success(()))
|
self.finish(.success(()))
|
||||||
} else {
|
|
||||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
self.finish(.failure(ALTServerError(.underlyingError)))
|
print("IPA doesn't exist????")
|
||||||
|
self.finish(.failure(OperationError(.appNotFound(name: resignedApp.name))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,48 +8,87 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
enum VerificationError: ALTLocalizedError
|
extension VerificationError
|
||||||
{
|
{
|
||||||
case privateEntitlements(ALTApplication, entitlements: [String: Any])
|
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||||
case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String)
|
typealias Error = VerificationError
|
||||||
case iOSVersionNotSupported(ALTApplication)
|
|
||||||
|
case privateEntitlements
|
||||||
|
case mismatchedBundleIdentifiers
|
||||||
|
case iOSVersionNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError {
|
||||||
|
VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
||||||
|
VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
|
||||||
|
VerificationError(code: .iOSVersionNotSupported, app: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VerificationError: ALTLocalizedError {
|
||||||
|
let code: Code
|
||||||
|
|
||||||
|
var errorTitle: String?
|
||||||
|
var errorFailure: String?
|
||||||
|
|
||||||
|
@Managed var app: AppProtocol?
|
||||||
|
var entitlements: [String: Any]?
|
||||||
|
var sourceBundleID: String?
|
||||||
|
var deviceOSVersion: OperatingSystemVersion?
|
||||||
|
var requiredOSVersion: OperatingSystemVersion?
|
||||||
|
|
||||||
var app: ALTApplication {
|
var errorDescription: String? {
|
||||||
switch self
|
switch self.code {
|
||||||
{
|
case .iOSVersionNotSupported:
|
||||||
case .privateEntitlements(let app, _): return app
|
guard let deviceOSVersion else { return nil }
|
||||||
case .mismatchedBundleIdentifiers(let app, _): return app
|
|
||||||
case .iOSVersionNotSupported(let app): return app
|
var failureReason = self.errorFailureReason
|
||||||
|
if self.app == nil {
|
||||||
|
let firstLetter = failureReason.prefix(1).lowercased()
|
||||||
|
failureReason = firstLetter + failureReason.dropFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(formatted: "This device is running iOS %@, but %@", deviceOSVersion.stringValue, failureReason)
|
||||||
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var failure: String? {
|
var errorFailureReason: String {
|
||||||
return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name)
|
switch self.code
|
||||||
}
|
|
||||||
|
|
||||||
var failureReason: String? {
|
|
||||||
switch self
|
|
||||||
{
|
{
|
||||||
case .privateEntitlements(let app, _):
|
case .privateEntitlements:
|
||||||
return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name)
|
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||||
|
return String(formatted: "“%@” requires private permissions.", appName)
|
||||||
case .mismatchedBundleIdentifiers(let app, let sourceBundleID):
|
|
||||||
return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, sourceBundleID)
|
case .mismatchedBundleIdentifiers:
|
||||||
|
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID {
|
||||||
case .iOSVersionNotSupported(let app):
|
return String(formatted: "The bundle ID '%@' does not match the one specified by the source ('%@').", appBundleID, bundleID)
|
||||||
let name = app.name
|
} else {
|
||||||
|
return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
|
||||||
var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)"
|
}
|
||||||
if app.minimumiOSVersion.patchVersion > 0
|
|
||||||
{
|
case .iOSVersionNotSupported:
|
||||||
version += ".\(app.minimumiOSVersion.patchVersion)"
|
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||||
|
let deviceOSVersion = self.deviceOSVersion ?? ProcessInfo.processInfo.operatingSystemVersion
|
||||||
|
|
||||||
|
guard let requiredOSVersion else {
|
||||||
|
return String(formatted: "%@ does not support iOS %@.", appName, deviceOSVersion.stringValue)
|
||||||
|
}
|
||||||
|
if deviceOSVersion > requiredOSVersion {
|
||||||
|
return String(formatted: "%@ requires iOS %@ or earlier", appName, requiredOSVersion.stringValue)
|
||||||
|
} else {
|
||||||
|
return String(formatted: "%@ requires iOS %@ or later", appName, requiredOSVersion.stringValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
|
|
||||||
return localizedDescription
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,12 +119,14 @@ final class VerifyAppOperation: ResultOperation<Void>
|
|||||||
|
|
||||||
guard let app = self.context.app else { throw OperationError.invalidParameters }
|
guard let app = self.context.app else { throw OperationError.invalidParameters }
|
||||||
|
|
||||||
guard app.bundleIdentifier == self.context.bundleIdentifier else {
|
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
|
||||||
throw VerificationError.mismatchedBundleIdentifiers(app, sourceBundleID: self.context.bundleIdentifier)
|
guard app.bundleIdentifier == self.context.bundleIdentifier else {
|
||||||
|
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
|
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
|
||||||
throw VerificationError.iOSVersionNotSupported(app)
|
throw VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: app.minimumiOSVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 13.5, *)
|
if #available(iOS 13.5, *)
|
||||||
@@ -116,7 +157,7 @@ final class VerifyAppOperation: ResultOperation<Void>
|
|||||||
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
|
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
|
||||||
|
|
||||||
app.hasPrivateEntitlements = true
|
app.hasPrivateEntitlements = true
|
||||||
let error = VerificationError.privateEntitlements(app, entitlements: entitlements)
|
let error = VerificationError.privateEntitlements(entitlements, app: app)
|
||||||
self.process(error) { (result) in
|
self.process(error) { (result) in
|
||||||
self.finish(result.mapError { $0 as Error })
|
self.finish(result.mapError { $0 as Error })
|
||||||
}
|
}
|
||||||
@@ -145,9 +186,10 @@ private extension VerifyAppOperation
|
|||||||
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
|
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
switch error
|
switch error.code
|
||||||
{
|
{
|
||||||
case .privateEntitlements(_, let entitlements):
|
case .privateEntitlements:
|
||||||
|
guard let entitlements = error.entitlements else { return completion(.failure(error)) }
|
||||||
let permissions = entitlements.keys.sorted().joined(separator: "\n")
|
let permissions = entitlements.keys.sorted().joined(separator: "\n")
|
||||||
let message = String(format: NSLocalizedString("""
|
let message = String(format: NSLocalizedString("""
|
||||||
You must allow access to these private permissions before continuing:
|
You must allow access to these private permissions before continuing:
|
||||||
@@ -166,8 +208,7 @@ private extension VerifyAppOperation
|
|||||||
}))
|
}))
|
||||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||||
|
|
||||||
case .mismatchedBundleIdentifiers: return completion(.failure(error))
|
case .mismatchedBundleIdentifiers, .iOSVersionNotSupported: return completion(.failure(error))
|
||||||
case .iOSVersionNotSupported: return completion(.failure(error))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,158 @@
|
|||||||
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]}
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "40.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "60.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "29.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "58.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "87.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "80.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "120.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "57.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "57x57"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "114.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "57x57"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "120.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "180.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "20.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "40.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "29.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "58.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "40.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "80.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "50.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "50x50"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "100.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "50x50"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "72.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "72x72"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "144.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "72x72"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "76.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "152.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "167.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "1024.png",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "riley.jpg",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 73 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "shane.jpeg",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 43 KiB |
BIN
AltStore/Resources/Assets.xcassets/SideStore.imageset/1024.png
vendored
Normal file
|
After Width: | Height: | Size: 846 KiB |
21
AltStore/Resources/Assets.xcassets/SideStore.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,18 +3,18 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>application-identifier</key>
|
<key>application-identifier</key>
|
||||||
<string>A72ZC8AJ5X.com.SideStore.AltStore</string>
|
<string>XYZ0123456.com.SideStore.SideStore</string>
|
||||||
<key>aps-environment</key>
|
<key>aps-environment</key>
|
||||||
<string>development</string>
|
<string>development</string>
|
||||||
<key>com.apple.developer.siri</key>
|
<key>com.apple.developer.siri</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.developer.team-identifier</key>
|
<key>com.apple.developer.team-identifier</key>
|
||||||
<string>A72ZC8AJ5X</string>
|
<string>XYZ0123456</string>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.com.SideStore.AltStore</string>
|
<string>group.com.SideStore.SideStore</string>
|
||||||
</array>
|
</array>
|
||||||
<key>get-task-allow</key>
|
<key>get-task-allow</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "AltStore",
|
|
||||||
"identifier": "com.rileytestut.AltStore",
|
|
||||||
"sourceURL": "https://cdn.altstore.io/file/altstore/apps.json",
|
|
||||||
"apps": [
|
|
||||||
{
|
|
||||||
"name": "AltStore",
|
|
||||||
"bundleIdentifier": "com.rileytestut.AltStore",
|
|
||||||
"developerName": "Riley Testut",
|
|
||||||
"version": "1.5.1",
|
|
||||||
"versionDate": "2022-07-14T12:00:00-05:00",
|
|
||||||
"versionDescription": "This update fixes the following issues:\n\n• Using Apple IDs that contain capital letters\n• Using Apple IDs with 2FA enabled without any trusted devices\n• Repeatedly asking some users to sign in every refresh\n• \"Incorrect Apple ID or password\" error after changing Apple ID email address\n• “Application is missing application-identifier” error when sideloading or (de-)activating certain apps\n• Potential crash when receiving unknown error codes from AltServer",
|
|
||||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_5_1.ipa",
|
|
||||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis version of AltStore allows you to install Delta, an all-in-one emulator for iOS, as well as sideload other .ipa files from the Files app.",
|
|
||||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
|
||||||
"tintColor": "018084",
|
|
||||||
"size": 5465976,
|
|
||||||
"screenshotURLs": [
|
|
||||||
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",
|
|
||||||
"https://user-images.githubusercontent.com/705880/78942222-0fe6da00-7a6e-11ea-9f2a-dda16157583c.PNG",
|
|
||||||
"https://user-images.githubusercontent.com/705880/65605577-332cba80-df5e-11e9-9f00-b369ce974f71.PNG"
|
|
||||||
],
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"type": "background-fetch",
|
|
||||||
"usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "background-audio",
|
|
||||||
"usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AltStore",
|
|
||||||
"bundleIdentifier": "com.rileytestut.AltStore.Beta",
|
|
||||||
"developerName": "Riley Testut",
|
|
||||||
"subtitle": "An alternative App Store for iOS.",
|
|
||||||
"version": "1.6b2",
|
|
||||||
"versionDate": "2022-09-21T13:00:00-05:00",
|
|
||||||
"versionDescription": "• Fixed “error migrating persistent store” issue on launch\n\nPREVIOUS VERSION\n\nLock Screen Widget (iOS 16+)\n• Counts down days until AltStore expires\n• Comes in 2 different styles: “icon” and “text”\n\nError Log\n• View past errors in more detail\n• Tap an error to copy the error message or error code\n• Search for error code directly in AltStore FAQ",
|
|
||||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_6_b2.ipa",
|
|
||||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore adds support for 3rd party sources, allowing you to download apps from other developers directly through AltStore.",
|
|
||||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
|
||||||
"tintColor": "018084",
|
|
||||||
"size": 5465933,
|
|
||||||
"beta": true,
|
|
||||||
"screenshotURLs": [
|
|
||||||
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",
|
|
||||||
"https://user-images.githubusercontent.com/705880/78942222-0fe6da00-7a6e-11ea-9f2a-dda16157583c.PNG",
|
|
||||||
"https://user-images.githubusercontent.com/705880/65605577-332cba80-df5e-11e9-9f00-b369ce974f71.PNG"
|
|
||||||
],
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"type": "background-fetch",
|
|
||||||
"usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "background-audio",
|
|
||||||
"usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delta",
|
|
||||||
"bundleIdentifier": "com.rileytestut.Delta",
|
|
||||||
"developerName": "Riley Testut",
|
|
||||||
"subtitle": "Classic games in your pocket.",
|
|
||||||
"version": "1.3.1",
|
|
||||||
"versionDate": "2021-12-02T13:30:00-08:00",
|
|
||||||
"versionDescription": "• Fixes game artwork not loading\n• Fixes using deprecated DeSmuME core over melonDS core for some users",
|
|
||||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/delta/1_3_1.ipa",
|
|
||||||
"localizedDescription": "Delta is an all-in-one emulator for iOS. Delta builds upon the strengths of its predecessor, GBA4iOS, while expanding to include support for more game systems such as NES, SNES, and N64.\n\nFEATURES\n\nSupported Game Systems\n• Nintendo Entertainment System\n• Super Nintendo Entertainment System\n• Nintendo 64\n• Game Boy (Color)\n• Game Boy Advance\n• Nintendo DS\n• And plenty more to come!\n\nController Support\n• Supports PS4, PS5, Xbox One S, Xbox Series X, and MFi game controllers.\n• Supports bluetooth (and wired) keyboards, as well as the Apple Smart Keyboard.\n• Completely customize button mappings on a per-system, per-controller basis.\n• Map buttons to special “Quick Save”, “Quick Load,” and “Fast Forward” actions.\n\nSave States\n• Save and load save states for any game from the pause menu.\n• Lock save states to prevent them from being accidentally overwritten.\n• Automatically makes backup save states to ensure you never lose your progress.\n• Support for “Quick Saves,” save states that can be quickly saved/loaded with a single button press (requires external controller).\n\nCheats\n• Supports various types of cheat codes for each supported system:\n• NES: Game Genie\n• SNES: Game Genie, Pro Action Replay\n• N64: GameShark\n• GBC: Game Genie, GameShark\n• GBA: Action Replay, Code Breaker, GameShark\n• DS: Action Replay\n\nDelta Sync\n• Sync your games, game saves, save states, cheats, controller skins, and controller mappings between devices.\n• View version histories of everything you sync and optionally restore them to earlier versions.\n• Supports both Google Drive and Dropbox.\n\nCustom Controller Skins\n• Beautiful built-in controller skins for all systems.\n• Import controller skins made by others, or even make your own to share with the world!\n\nHold Button\n• Choose buttons for Delta to hold down on your behalf, freeing up your thumbs to press other buttons instead.\n• Perfect for games that typically require one button be held down constantly (ex: run button in Mario games, or the A button in Mario Kart).\n\nFast Forward\n• Speed through slower parts of games by running the game much faster than normal.\n• Easily enable or disable from the pause menu, or optionally with a mapped button on an external controller.\n\n3D/Haptic Touch\n• Use 3D or Haptic Touch to “peek” at games, save states, and cheat codes.\n• App icon shortcuts allow quick access to your most recently played games, or optionally customize the shortcuts to always include certain games.\n\nGame Artwork\n• Automatically displays appropriate box art for imported games.\n• Change a game’s artwork to anything you want, or select from the built-in game artwork database.\n\nMisc.\n• Gyroscope support (WarioWare: Twisted! only)\n• Microphone support (DS only)\n• Support for delta:// URL scheme to jump directly into a specific game.\n\n**Delta and AltStore LLC are in no way affiliated with Nintendo. The name \"Nintendo\" and all associated game console names are registered trademarks of Nintendo Co., Ltd.**",
|
|
||||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
|
|
||||||
"tintColor": "8A28F7",
|
|
||||||
"size": 19739373,
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"type": "photos",
|
|
||||||
"usageDescription": "Allows Delta to use images from your Photo Library as game artwork."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"screenshotURLs": [
|
|
||||||
"https://user-images.githubusercontent.com/705880/65600448-f7d9be00-df54-11e9-9e3e-d4c31296da94.PNG",
|
|
||||||
"https://user-images.githubusercontent.com/705880/65813009-f2ae8600-e183-11e9-9eb7-704effc11173.png",
|
|
||||||
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG",
|
|
||||||
"https://user-images.githubusercontent.com/705880/65601125-5b182000-df56-11e9-9e7e-261480e893c0.PNG"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delta",
|
|
||||||
"bundleIdentifier": "com.rileytestut.Delta.Beta",
|
|
||||||
"developerName": "Riley Testut",
|
|
||||||
"subtitle": "Classic games in your pocket.",
|
|
||||||
"version": "1.4b2",
|
|
||||||
"versionDate": "2022-08-16T08:00:00-05:00",
|
|
||||||
"versionDescription": "NEW\n• Supports Split View and Stage Manager multitasking on iPad\n• Automatically pauses + resumes emulation when switching between foreground apps with Stage Manager\n• Optimized full screen-width controller skins when using Split View, Slide Over, or Stage Manager\n• Supports controller skins with new `placement` parameter\n• Supports controller skins with custom screens that don’t have explicit `outputFrame`\n\nFIXED\n• Fixed not detecting keyboard presses when remapping inputs\n• Fixed potential crash rendering game screen after changing EAGLContext\n• Fixed incorrect game screen frame when software keyboard appears on iOS 16\n• Fixed software keyboard sometimes appearing when not emulating anything",
|
|
||||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/delta/1_4_b2.ipa",
|
|
||||||
"localizedDescription": "The next consoles for Delta are coming: this beta version of Delta brings support for playing Nintendo DS and Sega Genesis games!\n\nPlease report any issues you find to support@altstore.io. Thanks!",
|
|
||||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
|
|
||||||
"tintColor": "8A28F7",
|
|
||||||
"size": 42968657,
|
|
||||||
"beta": true,
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"type": "photos",
|
|
||||||
"usageDescription": "Allows Delta to use images from your Photo Library as game artwork."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"screenshotURLs": [
|
|
||||||
"https://user-images.githubusercontent.com/705880/65600448-f7d9be00-df54-11e9-9e3e-d4c31296da94.PNG",
|
|
||||||
"https://user-images.githubusercontent.com/705880/65601942-e5ad4f00-df57-11e9-9255-1463e0296e46.PNG",
|
|
||||||
"https://user-images.githubusercontent.com/705880/65813009-f2ae8600-e183-11e9-9eb7-704effc11173.png",
|
|
||||||
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Clip",
|
|
||||||
"bundleIdentifier": "com.rileytestut.Clip",
|
|
||||||
"subtitle": "Manage your clipboard history with ease.",
|
|
||||||
"developerName": "Riley Testut",
|
|
||||||
"version": "1.0",
|
|
||||||
"versionDate": "2020-06-17T12:30:00-07:00",
|
|
||||||
"versionDescription": "Initial version 🎉",
|
|
||||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/clip/1_0.ipa",
|
|
||||||
"localizedDescription": "Clip is a simple clipboard manager for iOS. \n\nUnlike other clipboard managers, Clip can continue monitoring your clipboard while in the background. No longer do you need to remember to manually open or share to an app to save your clipboard; just copy and paste as you would normally do, and Clip will have your back.\n\nIn addition to background monitoring, Clip also has these features:\n\n• Save text, URLs, and images copied to the clipboard.\n• Copy, delete, or share any clippings saved to Clip.\n• Customizable history limit.\n\nDownload Clip today, and never worry about losing your clipboard again!",
|
|
||||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391981-5326f800-c37a-11e9-99d8-760fd06bb601.png",
|
|
||||||
"tintColor": "EC008C",
|
|
||||||
"size": 445056,
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"type": "background-audio",
|
|
||||||
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"screenshotURLs": [
|
|
||||||
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png",
|
|
||||||
"https://user-images.githubusercontent.com/705880/70830209-8e738980-1da4-11ea-8b3b-6e5fbc78adff.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Clip",
|
|
||||||
"bundleIdentifier": "com.rileytestut.Clip.Beta",
|
|
||||||
"subtitle": "Manage your clipboard history with ease.",
|
|
||||||
"developerName": "Riley Testut",
|
|
||||||
"version": "1.1b1",
|
|
||||||
"versionDate": "2020-06-17T12:30:00-07:00",
|
|
||||||
"versionDescription": "This update adds a Custom Keyboard app extension for quick access to clippings when editing text.\n\nTo enable the keyboard, go to Settings > General > Keyboard > Keyboards > Add New Keyboard... and add \"ClipBoard\". Once added, make sure to then enable \"Allow Full Access\" for ClipBoard so it can access your clippings.",
|
|
||||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/clip/1_1_b1.ipa",
|
|
||||||
"localizedDescription": "Clip is a simple clipboard manager for iOS. \n\nUnlike other clipboard managers, Clip can continue monitoring your clipboard while in the background. No longer do you need to remember to manually open or share to an app to save your clipboard; just copy and paste as you would normally do, and Clip will have your back.\n\nIn addition to background monitoring, Clip also has these features:\n\n• Save text, URLs, and images copied to the clipboard.\n• Copy, delete, or share any clippings saved to Clip.\n• Customizable history limit.\n\nDownload Clip today, and never worry about losing your clipboard again!",
|
|
||||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391981-5326f800-c37a-11e9-99d8-760fd06bb601.png",
|
|
||||||
"tintColor": "EC008C",
|
|
||||||
"size": 462771,
|
|
||||||
"beta": true,
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"type": "background-audio",
|
|
||||||
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"screenshotURLs": [
|
|
||||||
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png",
|
|
||||||
"https://user-images.githubusercontent.com/705880/70830209-8e738980-1da4-11ea-8b3b-6e5fbc78adff.png",
|
|
||||||
"https://user-images.githubusercontent.com/705880/84842227-70a80b00-aff9-11ea-8b04-bedb1f49c4a7.PNG",
|
|
||||||
"https://user-images.githubusercontent.com/705880/84842231-7271ce80-aff9-11ea-9272-e128aeceb95b.PNG"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"news": [
|
|
||||||
{
|
|
||||||
"title": "Delta Gaining DS Support",
|
|
||||||
"identifier": "delta-ds-support",
|
|
||||||
"caption": "Available this Saturday for patrons, coming soon for everyone else.",
|
|
||||||
"tintColor": "8A28F7",
|
|
||||||
"imageURL": "https://user-images.githubusercontent.com/705880/65603159-0676a400-df5a-11e9-882e-dc5566f4d50a.png",
|
|
||||||
"date": "2019-09-25",
|
|
||||||
"notify": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Delta Now Available",
|
|
||||||
"identifier": "delta-now-available",
|
|
||||||
"caption": "Finally, relive your favorite NES, SNES, GB(C), GBA, and N64 games.",
|
|
||||||
"tintColor": "8A28F7",
|
|
||||||
"imageURL": "https://user-images.githubusercontent.com/705880/65604130-c1ec0800-df5b-11e9-8150-7657c474e3c3.png",
|
|
||||||
"appID": "com.rileytestut.Delta",
|
|
||||||
"date": "2019-09-28",
|
|
||||||
"notify": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Sideloading is Here!",
|
|
||||||
"identifier": "sideloading-is-here",
|
|
||||||
"caption": "Update to AltStore 1.3 to install any app directly from Files.",
|
|
||||||
"tintColor": "018084",
|
|
||||||
"imageURL": "https://user-images.githubusercontent.com/705880/79022069-02932380-7b32-11ea-8bad-49907cb97ece.png",
|
|
||||||
"date": "2020-04-10T13:00:00-07:00",
|
|
||||||
"notify": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "iOS 13.4 Fixes App Crashes",
|
|
||||||
"identifier": "ios-13-4-now-available",
|
|
||||||
"caption": "Update to iOS 13.4 to fix some sideloaded apps crashing on launch.",
|
|
||||||
"tintColor": "34C759",
|
|
||||||
"date": "2020-04-10T13:30:00-07:00",
|
|
||||||
"notify": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Clip Now Available!",
|
|
||||||
"identifier": "clip-now-available",
|
|
||||||
"caption": "Finally, a clipboard manager that can run in the background — no jailbreak required.",
|
|
||||||
"tintColor": "EC008C",
|
|
||||||
"imageURL": "https://user-images.githubusercontent.com/705880/65606598-04afdf00-df60-11e9-8f93-af6345d39557.png",
|
|
||||||
"appID": "com.rileytestut.Clip",
|
|
||||||
"date": "2020-06-17",
|
|
||||||
"notify": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Delta, Meet Nintendo DS",
|
|
||||||
"identifier": "delta-meet-ds",
|
|
||||||
"caption": "Update to Delta 1.3 to relive all your favorite Nintendo DS games.",
|
|
||||||
"tintColor": "8A28F7",
|
|
||||||
"imageURL": "https://user-images.githubusercontent.com/705880/115617602-6ce2b600-a2a6-11eb-984e-2197a30c71e2.png",
|
|
||||||
"appID": "com.rileytestut.Delta",
|
|
||||||
"date": "2021-04-21",
|
|
||||||
"notify": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "#StandWithUkraine",
|
|
||||||
"identifier": "support-ukraine",
|
|
||||||
"caption": "Find out how you can help support those impacted by the Russian invasion.",
|
|
||||||
"tintColor": "003e80",
|
|
||||||
"imageURL": "https://user-images.githubusercontent.com/705880/156053447-a158cac7-df5f-4497-8025-15c3c2e10b48.png",
|
|
||||||
"url": "https://linktr.ee/razomforukraine",
|
|
||||||
"date": "2022-03-01",
|
|
||||||
"notify": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "The Biggest AltServer Update Yet!",
|
|
||||||
"identifier": "altserver-1-5",
|
|
||||||
"caption": "Update to AltServer 1.5 to use AltJIT and other exciting new features.",
|
|
||||||
"tintColor": "018084",
|
|
||||||
"imageURL": "https://user-images.githubusercontent.com/705880/166509576-744be578-6868-4b7d-b4fd-b9418c084327.png",
|
|
||||||
"url": "https://faq.altstore.io/release-notes/altserver",
|
|
||||||
"date": "2022-05-03",
|
|
||||||
"notify": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "More Apps in AltStore!",
|
|
||||||
"identifier": "trusted-sources",
|
|
||||||
"caption": "Update to AltStore 1.5 to easily download some of our favorite apps.",
|
|
||||||
"tintColor": "00CAB3",
|
|
||||||
"imageURL": "https://user-images.githubusercontent.com/705880/167026375-ddcb004f-7160-405c-b3e3-87a6795d2f43.png",
|
|
||||||
"url": "https://faq.altstore.io/release-notes/altstore",
|
|
||||||
"date": "2022-05-05",
|
|
||||||
"notify": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "New to AltStore?",
|
|
||||||
"identifier": "updated-faq",
|
|
||||||
"caption": "Check out our updated guide to learn how to sideload apps!",
|
|
||||||
"tintColor": "018084",
|
|
||||||
"url": "https://faq.altstore.io",
|
|
||||||
"date": "2022-07-28",
|
|
||||||
"notify": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"userInfo": {
|
|
||||||
"patreonAccessToken": "uqoDoTxH8dY1ImE8tK76wxrzKk67gjyjBAcK8sD3RLU"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>StringsTable</key>
|
|
||||||
<string>Root</string>
|
|
||||||
<key>ApplicationGroupContainerIdentifier</key>
|
|
||||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
|
||||||
<key>PreferenceSpecifiers</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>Type</key>
|
|
||||||
<string>PSMultiValueSpecifier</string>
|
|
||||||
<key>Title</key>
|
|
||||||
<string>Anisette Server</string>
|
|
||||||
<key>Key</key>
|
|
||||||
<string>customAnisetteURL</string>
|
|
||||||
<key>DefaultValue</key>
|
|
||||||
<string>https://ani.sidestore.io</string>
|
|
||||||
<key>Titles</key>
|
|
||||||
<array>
|
|
||||||
<string>SideStore</string>
|
|
||||||
<string>Macley (US)</string>
|
|
||||||
<string>Macley (DE)</string>
|
|
||||||
<string>DrPudding</string>
|
|
||||||
<string>Sideloadly</string>
|
|
||||||
<string>Nick</string>
|
|
||||||
<string>Jawshoeadan</string>
|
|
||||||
<string>crystall1nedev</string>
|
|
||||||
</array>
|
|
||||||
<key>Values</key>
|
|
||||||
<array>
|
|
||||||
<string>https://ani.sidestore.io</string>
|
|
||||||
<string>http://us1.sternserv.tech</string>
|
|
||||||
<string>http://de1.sternserv.tech</string>
|
|
||||||
<string>https://sign.rheaa.xyz</string>
|
|
||||||
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
|
|
||||||
<string>http://45.33.29.114</string>
|
|
||||||
<string>https://anisette.jawshoeadan.me</string>
|
|
||||||
<string>https://anisette.crystall1ne.software/</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>Type</key>
|
|
||||||
<string>PSGroupSpecifier</string>
|
|
||||||
<key>Title</key>
|
|
||||||
<string>Danger Zone</string>
|
|
||||||
<key>FooterText</key>
|
|
||||||
<string>If you disable the toggle then app will use the server you input into the "Anisette URL" box rather than one selected from the above selector.</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>Type</key>
|
|
||||||
<string>PSToggleSwitchSpecifier</string>
|
|
||||||
<key>Title</key>
|
|
||||||
<string>Use preferred servers</string>
|
|
||||||
<key>Key</key>
|
|
||||||
<string>textServer</string>
|
|
||||||
<key>DefaultValue</key>
|
|
||||||
<true/>
|
|
||||||
<key>FooterText</key>
|
|
||||||
<string>chicken</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>Type</key>
|
|
||||||
<string>PSTextFieldSpecifier</string>
|
|
||||||
<key>Title</key>
|
|
||||||
<string>Anisette URL</string>
|
|
||||||
<key>Key</key>
|
|
||||||
<string>textInputAnisetteURL</string>
|
|
||||||
<key>AutocapitalizationType</key>
|
|
||||||
<string>None</string>
|
|
||||||
<key>AutocorrectionType</key>
|
|
||||||
<string>No</string>
|
|
||||||
<key>KeyboardType</key>
|
|
||||||
<string>URL</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="AboutHeader" id="xq2-Pl-zaG" customClass="AboutPatreonHeaderView" customModule="AltStore" customModuleProvider="target">
|
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="AboutHeader" id="xq2-Pl-zaG" customClass="AboutPatreonHeaderView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="445"/>
|
<rect key="frame" x="0.0" y="0.0" width="390" height="682"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="25" translatesAutoresizingMaskIntoConstraints="NO" id="XiA-Jf-XMp">
|
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="25" translatesAutoresizingMaskIntoConstraints="NO" id="XiA-Jf-XMp">
|
||||||
<rect key="frame" x="16" y="2" width="343" height="393"/>
|
<rect key="frame" x="16" y="2" width="358" height="630"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="5Ol-zN-wYv">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="5Ol-zN-wYv">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="317"/>
|
<rect key="frame" x="0.0" y="0.0" width="358" height="426"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="f7H-EV-7Sx">
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="f7H-EV-7Sx">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="55"/>
|
<rect key="frame" x="0.0" y="0.0" width="358" height="55"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="SideStore" translatesAutoresizingMaskIntoConstraints="NO" id="pn6-Ic-MJm">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="SideStore" translatesAutoresizingMaskIntoConstraints="NO" id="pn6-Ic-MJm">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="55" height="55"/>
|
<rect key="frame" x="0.0" y="0.0" width="55" height="55"/>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</constraints>
|
</constraints>
|
||||||
</imageView>
|
</imageView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="si2-MA-3RH">
|
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="si2-MA-3RH">
|
||||||
<rect key="frame" x="65" y="0.0" width="278" height="55"/>
|
<rect key="frame" x="65" y="0.0" width="293" height="55"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="hkS-oz-wiT">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="hkS-oz-wiT">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="83" height="55"/>
|
<rect key="frame" x="0.0" y="0.0" width="83" height="55"/>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="TFB-qo-cbh">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="TFB-qo-cbh">
|
||||||
<rect key="frame" x="195" y="0.0" width="83" height="55"/>
|
<rect key="frame" x="210" y="0.0" width="83" height="55"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Zpb-k3-y7l">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Zpb-k3-y7l">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="83" height="50"/>
|
<rect key="frame" x="0.0" y="0.0" width="83" height="50"/>
|
||||||
@@ -75,11 +75,13 @@
|
|||||||
</constraints>
|
</constraints>
|
||||||
</stackView>
|
</stackView>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FeG-e5-LJl">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FeG-e5-LJl">
|
||||||
<rect key="frame" x="0.0" y="65" width="343" height="252"/>
|
<rect key="frame" x="0.0" y="65" width="358" height="361"/>
|
||||||
<color key="backgroundColor" white="1" alpha="0.13" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="1" alpha="0.13" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<string key="text">Hello, thank you for using SideStore!
|
<string key="text">Thank you for using SideStore!
|
||||||
|
|
||||||
If you would subscribe to the patreon that would support us and make sure we can continue developing SideStore for you.
|
Subscribing to the patreon supports us and makes sure we can continue developing SideStore for you.
|
||||||
|
|
||||||
|
Following us on social media allows us to give quick updates and spread the word about sideloading!
|
||||||
|
|
||||||
-SideTeam</string>
|
-SideTeam</string>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
@@ -89,10 +91,10 @@ If you would subscribe to the patreon that would support us and make sure we can
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="13" translatesAutoresizingMaskIntoConstraints="NO" id="QS9-vO-bj8">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="13" translatesAutoresizingMaskIntoConstraints="NO" id="QS9-vO-bj8">
|
||||||
<rect key="frame" x="0.0" y="342" width="343" height="51"/>
|
<rect key="frame" x="0.0" y="451" width="358" height="179"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yEi-L6-kQ8">
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yEi-L6-kQ8">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="358" height="51"/>
|
||||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="51" id="l4o-vb-cMy"/>
|
<constraint firstAttribute="height" constant="51" id="l4o-vb-cMy"/>
|
||||||
@@ -102,6 +104,28 @@ If you would subscribe to the patreon that would support us and make sure we can
|
|||||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</state>
|
</state>
|
||||||
</button>
|
</button>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hov-Ce-LaM" userLabel="Twitter Button">
|
||||||
|
<rect key="frame" x="0.0" y="64" width="358" height="51"/>
|
||||||
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="51" id="m0M-GX-KKG"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
|
||||||
|
<state key="normal" title="Follow us on Twitter!">
|
||||||
|
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</state>
|
||||||
|
</button>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="VdY-7Q-amF" userLabel="Twitter Button">
|
||||||
|
<rect key="frame" x="0.0" y="128" width="358" height="51"/>
|
||||||
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="51" id="kDo-b8-6tZ"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
|
||||||
|
<state key="normal" title="Follow us on Instagram!">
|
||||||
|
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</state>
|
||||||
|
</button>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
@@ -114,19 +138,21 @@ If you would subscribe to the patreon that would support us and make sure we can
|
|||||||
<constraint firstItem="XiA-Jf-XMp" firstAttribute="top" secondItem="xq2-Pl-zaG" secondAttribute="top" constant="2" id="j8p-JX-Dcz"/>
|
<constraint firstItem="XiA-Jf-XMp" firstAttribute="top" secondItem="xq2-Pl-zaG" secondAttribute="top" constant="2" id="j8p-JX-Dcz"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
|
<outlet property="instagramButton" destination="VdY-7Q-amF" id="5kj-9x-k4F"/>
|
||||||
<outlet property="rileyImageView" destination="pn6-Ic-MJm" id="60i-Q0-ojz"/>
|
<outlet property="rileyImageView" destination="pn6-Ic-MJm" id="60i-Q0-ojz"/>
|
||||||
<outlet property="rileyLabel" destination="DTd-Yu-HXr" id="O0y-JB-gWp"/>
|
<outlet property="rileyLabel" destination="DTd-Yu-HXr" id="O0y-JB-gWp"/>
|
||||||
<outlet property="shaneLabel" destination="Zpb-k3-y7l" id="aQN-6B-s5T"/>
|
<outlet property="shaneLabel" destination="Zpb-k3-y7l" id="aQN-6B-s5T"/>
|
||||||
<outlet property="supportButton" destination="yEi-L6-kQ8" id="Dzo-vd-SnD"/>
|
<outlet property="supportButton" destination="yEi-L6-kQ8" id="Dzo-vd-SnD"/>
|
||||||
<outlet property="textView" destination="FeG-e5-LJl" id="K0M-lF-I6u"/>
|
<outlet property="textView" destination="FeG-e5-LJl" id="K0M-lF-I6u"/>
|
||||||
|
<outlet property="twitterButton" destination="hov-Ce-LaM" id="gib-Lt-qtY"/>
|
||||||
</connections>
|
</connections>
|
||||||
<point key="canvasLocation" x="138" y="138"/>
|
<point key="canvasLocation" x="147.82608695652175" y="58.258928571428569"/>
|
||||||
</collectionReusableView>
|
</collectionReusableView>
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="SideStore" width="180" height="180"/>
|
<image name="SideStore" width="1024" height="1024"/>
|
||||||
<namedColor name="SettingsHighlighted">
|
<namedColor name="SettingsHighlighted">
|
||||||
<color red="0.23529411764705882" green="0.0" blue="0.40392156862745099" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.38823529411764707" green="0.011764705882352941" blue="0.58823529411764708" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import Foundation
|
|||||||
|
|
||||||
public struct AnisetteManager {
|
public struct AnisetteManager {
|
||||||
|
|
||||||
|
var menuURL: String {
|
||||||
|
var url: String
|
||||||
|
url = UserDefaults.standard.menuAnisetteURL
|
||||||
|
return url
|
||||||
|
}
|
||||||
/// User defined URL from Settings/UserDefaults
|
/// User defined URL from Settings/UserDefaults
|
||||||
static var userURL: String? {
|
static var userURL: String? {
|
||||||
var urlString: String?
|
var urlString: String?
|
||||||
|
|||||||
190
AltStore/Settings/AnisetteServerList.swift
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
//
|
||||||
|
// AnisetteServerList.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by ny on 6/18/24.
|
||||||
|
// Copyright © 2024 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
typealias SUIButton = SwiftUI.Button
|
||||||
|
|
||||||
|
// MARK: - AnisetteServerData
|
||||||
|
struct AnisetteServerData: Codable {
|
||||||
|
let servers: [Server]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Server
|
||||||
|
struct Server: Codable {
|
||||||
|
var name: String
|
||||||
|
var address: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnisetteViewModel: ObservableObject {
|
||||||
|
@Published var selected: String = ""
|
||||||
|
|
||||||
|
@Published var source: String = "https://servers.sidestore.io/servers.json"
|
||||||
|
@Published var servers: [Server] = []
|
||||||
|
|
||||||
|
func getListOfServers() {
|
||||||
|
guard let url = URL(string: source) else { return }
|
||||||
|
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let data = data {
|
||||||
|
do {
|
||||||
|
let decoder = Foundation.JSONDecoder()
|
||||||
|
let servers = try decoder.decode(AnisetteServerData.self, from: data)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.servers = servers.servers
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Handle decoding error
|
||||||
|
print("Failed to decode JSON: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AnisetteServers: View {
|
||||||
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
@StateObject var viewModel: AnisetteViewModel = AnisetteViewModel()
|
||||||
|
@State var selected: String? = nil
|
||||||
|
var errorCallback: () -> ()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color(UIColor.systemBackground)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onAppear {
|
||||||
|
viewModel.getListOfServers()
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
SwiftUI.List($viewModel.servers, id: \.address, selection: $selected) { server in
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("\(server.name.wrappedValue)")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text("\(server.address.wrappedValue)")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
if selected != nil {
|
||||||
|
if server.address.wrappedValue == selected {
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.onAppear {
|
||||||
|
UserDefaults.standard.menuAnisetteURL = server.address.wrappedValue
|
||||||
|
print(UserDefaults.synchronize(.standard)())
|
||||||
|
print(UserDefaults.standard.menuAnisetteURL)
|
||||||
|
print(server.address.wrappedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(RoundedRectangle(cornerRadius: 10).fill(Color(UIColor.secondarySystemBackground)))
|
||||||
|
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.listRowBackground(Color(UIColor.systemBackground))
|
||||||
|
} else {
|
||||||
|
List(selection: $selected) {
|
||||||
|
ForEach($viewModel.servers, id: \.name) { server in
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text("\(server.name.wrappedValue)")
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(alignment: .center)
|
||||||
|
Text("\(server.address.wrappedValue)")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(alignment: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(RoundedRectangle(cornerRadius: 10).fill(Color(UIColor.secondarySystemBackground)))
|
||||||
|
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
TextField("Anisette Server List", text: $viewModel.source)
|
||||||
|
.padding()
|
||||||
|
.background(RoundedRectangle(cornerRadius: 10).fill(Color(UIColor.secondarySystemFill)))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(height: 60)
|
||||||
|
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
|
||||||
|
.onChange(of: viewModel.source) { newValue in
|
||||||
|
UserDefaults.standard.menuAnisetteList = newValue
|
||||||
|
viewModel.getListOfServers()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
SUIButton(action: {
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
}) {
|
||||||
|
Text("Back")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.padding()
|
||||||
|
.background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.shadow(color: Color.accentColor.opacity(0.4), radius: 10, x: 0, y: 5)
|
||||||
|
|
||||||
|
SUIButton(action: {
|
||||||
|
viewModel.getListOfServers()
|
||||||
|
}) {
|
||||||
|
Text("Refresh Servers")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.padding()
|
||||||
|
.background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.shadow(color: Color.accentColor.opacity(0.4), radius: 10, x: 0, y: 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
SUIButton(action: {
|
||||||
|
#if !DEBUG
|
||||||
|
if Keychain.shared.adiPb != nil {
|
||||||
|
Keychain.shared.adiPb = nil
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
print("Cleared adi.pb from keychain")
|
||||||
|
errorCallback()
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
}) {
|
||||||
|
Text("Reset adi.pb")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.padding()
|
||||||
|
.background(RoundedRectangle(cornerRadius: 10).fill(Color.red))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.shadow(color: Color.red.opacity(0.4), radius: 10, x: 0, y: 5)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarHidden(true)
|
||||||
|
.navigationTitle("")
|
||||||
|
}
|
||||||
|
}
|
||||||
53
AltStore/Settings/Error Log/ErrorDetailsViewController.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// ErrorDetailsViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 10/5/22.
|
||||||
|
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
class ErrorDetailsViewController: UIViewController
|
||||||
|
{
|
||||||
|
var loggedError: LoggedError?
|
||||||
|
|
||||||
|
@IBOutlet private var textView: UITextView!
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
if let error = self.loggedError?.error
|
||||||
|
{
|
||||||
|
self.title = error.localizedErrorCode
|
||||||
|
|
||||||
|
let font = self.textView.font ?? UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
let detailedDescription = error.formattedDetailedDescription(with: font)
|
||||||
|
self.textView.attributedText = detailedDescription
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.title = NSLocalizedString("Error Details", comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.navigationController?.navigationBar.tintColor = .altPrimary
|
||||||
|
|
||||||
|
if #available(iOS 15, *), let sheetController = self.navigationController?.sheetPresentationController
|
||||||
|
{
|
||||||
|
sheetController.detents = [.medium(), .large()]
|
||||||
|
sheetController.selectedDetentIdentifier = .medium
|
||||||
|
sheetController.prefersGrabberVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews()
|
||||||
|
{
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
self.textView.textContainerInset.left = self.view.layoutMargins.left
|
||||||
|
self.textView.textContainerInset.right = self.view.layoutMargins.right
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,16 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
@objc(ErrorLogMenuButton)
|
||||||
|
private final class ErrorLogMenuButton: UIButton {
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
override func menuAttachmentPoint(for configuration: UIContextMenuConfiguration) -> CGPoint {
|
||||||
|
var point = super.menuAttachmentPoint(for: configuration)
|
||||||
|
point.y = self.bounds.midY
|
||||||
|
return point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc(ErrorLogTableViewCell)
|
@objc(ErrorLogTableViewCell)
|
||||||
final class ErrorLogTableViewCell: UITableViewCell
|
final class ErrorLogTableViewCell: UITableViewCell
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ final class ErrorLogViewController: UITableViewController
|
|||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
private var expandedErrorIDs = Set<NSManagedObjectID>()
|
private var expandedErrorIDs = Set<NSManagedObjectID>()
|
||||||
|
|
||||||
|
private var isScrolling = false {
|
||||||
|
didSet {
|
||||||
|
guard self.isScrolling != oldValue else { return }
|
||||||
|
self.updateButtonInteractivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private lazy var timeFormatter: DateFormatter = {
|
private lazy var timeFormatter: DateFormatter = {
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateStyle = .none
|
dateFormatter.dateStyle = .none
|
||||||
@@ -39,6 +46,15 @@ final class ErrorLogViewController: UITableViewController
|
|||||||
self.tableView.dataSource = self.dataSource
|
self.tableView.dataSource = self.dataSource
|
||||||
self.tableView.prefetchDataSource = self.dataSource
|
self.tableView.prefetchDataSource = self.dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
|
guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return }
|
||||||
|
|
||||||
|
let navigationController = segue.destination as! UINavigationController
|
||||||
|
|
||||||
|
let errorDetailsViewController = navigationController.viewControllers.first as! ErrorDetailsViewController
|
||||||
|
errorDetailsViewController.loggedError = loggedError
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ErrorLogViewController
|
private extension ErrorLogViewController
|
||||||
@@ -60,14 +76,8 @@ private extension ErrorLogViewController
|
|||||||
let cell = cell as! ErrorLogTableViewCell
|
let cell = cell as! ErrorLogTableViewCell
|
||||||
cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date)
|
cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date)
|
||||||
cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "")
|
cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "")
|
||||||
|
cell.errorCodeLabel.text = loggedError.error.localizedErrorCode
|
||||||
switch loggedError.domain
|
|
||||||
{
|
|
||||||
case AltServerErrorDomain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltServer Error %@", comment: ""), NSNumber(value: loggedError.code))
|
|
||||||
case OperationError.domain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltStore Error %@", comment: ""), NSNumber(value: loggedError.code))
|
|
||||||
default: cell.errorCodeLabel?.text = loggedError.error.localizedErrorCode
|
|
||||||
}
|
|
||||||
|
|
||||||
let nsError = loggedError.error as NSError
|
let nsError = loggedError.error as NSError
|
||||||
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
||||||
cell.errorDescriptionTextView.text = errorDescription
|
cell.errorDescriptionTextView.text = errorDescription
|
||||||
@@ -93,12 +103,19 @@ private extension ErrorLogViewController
|
|||||||
},
|
},
|
||||||
UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
|
UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
|
||||||
self?.searchFAQ(for: loggedError)
|
self?.searchFAQ(for: loggedError)
|
||||||
|
},
|
||||||
|
UIAction(title: NSLocalizedString("View More Details", comment: ""), image: UIImage(systemName: "ellipsis.circle")) { [weak self] _ in
|
||||||
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
cell.menuButton.menu = menu
|
cell.menuButton.menu = menu
|
||||||
|
cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
} else {
|
||||||
|
cell.menuButton.isUserInteractionEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include errorDescriptionTextView's text in cell summary.
|
// Include errorDescriptionTextView's text in cell summary.
|
||||||
cell.accessibilityLabel = [cell.errorFailureLabel.text, cell.dateLabel.text, cell.errorCodeLabel.text, cell.errorDescriptionTextView.text].compactMap { $0 }.joined(separator: ". ")
|
cell.accessibilityLabel = [cell.errorFailureLabel.text, cell.dateLabel.text, cell.errorCodeLabel.text, cell.errorDescriptionTextView.text].compactMap { $0 }.joined(separator: ". ")
|
||||||
|
|
||||||
@@ -232,22 +249,27 @@ private extension ErrorLogViewController
|
|||||||
|
|
||||||
func searchFAQ(for loggedError: LoggedError)
|
func searchFAQ(for loggedError: LoggedError)
|
||||||
{
|
{
|
||||||
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
|
let baseURL = URL(string: "https://faq.altstore.io/getting-started/error-codes")!
|
||||||
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
||||||
|
|
||||||
let query = [loggedError.domain, "\(loggedError.code)"].joined(separator: "+")
|
let query = [loggedError.domain, "\(loggedError.error.displayCode)"].joined(separator: "+")
|
||||||
components.queryItems = [URLQueryItem(name: "q", value: query)]
|
components.queryItems = [URLQueryItem(name: "q", value: query)]
|
||||||
|
|
||||||
let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
|
let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
|
||||||
safariViewController.preferredControlTintColor = .altPrimary
|
safariViewController.preferredControlTintColor = .altPrimary
|
||||||
self.present(safariViewController, animated: true)
|
self.present(safariViewController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func viewMoreDetails(for loggedError: LoggedError) {
|
||||||
|
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ErrorLogViewController
|
extension ErrorLogViewController
|
||||||
{
|
{
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||||
{
|
{
|
||||||
|
guard #unavailable(iOS 14) else { return }
|
||||||
let loggedError = self.dataSource.item(at: indexPath)
|
let loggedError = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||||
@@ -321,3 +343,32 @@ extension ErrorLogViewController: QLPreviewControllerDataSource {
|
|||||||
return fileURL as QLPreviewItem
|
return fileURL as QLPreviewItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ErrorLogViewController
|
||||||
|
{
|
||||||
|
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView)
|
||||||
|
{
|
||||||
|
self.isScrolling = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
|
||||||
|
{
|
||||||
|
self.isScrolling = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)
|
||||||
|
{
|
||||||
|
guard !decelerate else { return }
|
||||||
|
self.isScrolling = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateButtonInteractivity()
|
||||||
|
{
|
||||||
|
guard #available(iOS 14, *) else { return }
|
||||||
|
|
||||||
|
for case let cell as ErrorLogTableViewCell in self.tableView.visibleCells
|
||||||
|
{
|
||||||
|
cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ final class PatronsFooterView: UICollectionReusableView
|
|||||||
final class AboutPatreonHeaderView: UICollectionReusableView
|
final class AboutPatreonHeaderView: UICollectionReusableView
|
||||||
{
|
{
|
||||||
@IBOutlet var supportButton: UIButton!
|
@IBOutlet var supportButton: UIButton!
|
||||||
|
@IBOutlet var twitterButton: UIButton!
|
||||||
|
@IBOutlet var instagramButton: UIButton!
|
||||||
@IBOutlet var accountButton: UIButton!
|
@IBOutlet var accountButton: UIButton!
|
||||||
@IBOutlet var textView: UITextView!
|
@IBOutlet var textView: UITextView!
|
||||||
|
|
||||||
@@ -79,12 +81,12 @@ final class AboutPatreonHeaderView: UICollectionReusableView
|
|||||||
imageView.layer.cornerRadius = imageView.bounds.midY
|
imageView.layer.cornerRadius = imageView.bounds.midY
|
||||||
}
|
}
|
||||||
|
|
||||||
for button in [self.supportButton, self.accountButton].compactMap({ $0 })
|
for button in [self.supportButton, self.accountButton, self.twitterButton, self.instagramButton].compactMap({ $0 })
|
||||||
{
|
{
|
||||||
button.clipsToBounds = true
|
button.clipsToBounds = true
|
||||||
button.layer.cornerRadius = 16
|
button.layer.cornerRadius = 16
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutMarginsDidChange()
|
override func layoutMarginsDidChange()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ private extension PatreonViewController
|
|||||||
headerView.layoutMargins = self.view.layoutMargins
|
headerView.layoutMargins = self.view.layoutMargins
|
||||||
|
|
||||||
headerView.supportButton.addTarget(self, action: #selector(PatreonViewController.openPatreonURL(_:)), for: .primaryActionTriggered)
|
headerView.supportButton.addTarget(self, action: #selector(PatreonViewController.openPatreonURL(_:)), for: .primaryActionTriggered)
|
||||||
|
headerView.twitterButton.addTarget(self, action: #selector(PatreonViewController.openTwitterURL(_:)), for: .primaryActionTriggered)
|
||||||
|
headerView.instagramButton.addTarget(self, action: #selector(PatreonViewController.openInstagramURL(_:)), for: .primaryActionTriggered)
|
||||||
|
|
||||||
let defaultSupportButtonTitle = NSLocalizedString("Become a patron", comment: "")
|
let defaultSupportButtonTitle = NSLocalizedString("Become a patron", comment: "")
|
||||||
let isPatronSupportButtonTitle = NSLocalizedString("View Patreon", comment: "")
|
let isPatronSupportButtonTitle = NSLocalizedString("View Patreon", comment: "")
|
||||||
|
|
||||||
@@ -180,6 +182,24 @@ private extension PatreonViewController
|
|||||||
self.present(safariViewController, animated: true, completion: nil)
|
self.present(safariViewController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openTwitterURL(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
let twitterURL = URL(string: "https://twitter.com/SideStore_io")!
|
||||||
|
|
||||||
|
let safariViewController = SFSafariViewController(url: twitterURL)
|
||||||
|
safariViewController.preferredControlTintColor = self.view.tintColor
|
||||||
|
self.present(safariViewController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func openInstagramURL(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
let twitterURL = URL(string: "https://instagram.com/sidestore.io")!
|
||||||
|
|
||||||
|
let safariViewController = SFSafariViewController(url: twitterURL)
|
||||||
|
safariViewController.preferredControlTintColor = self.view.tintColor
|
||||||
|
self.present(safariViewController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
@IBAction func authenticate(_ sender: UIBarButtonItem)
|
@IBAction func authenticate(_ sender: UIBarButtonItem)
|
||||||
{
|
{
|
||||||
PatreonAPI.shared.authenticate { (result) in
|
PatreonAPI.shared.authenticate { (result) in
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23090" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23079"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
<color key="backgroundColor" name="SettingsBackground"/>
|
<color key="backgroundColor" name="SettingsBackground"/>
|
||||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore 1.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2">
|
<label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore 1.0" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2">
|
||||||
<rect key="frame" x="0.0" y="1082" width="375" height="25"/>
|
<rect key="frame" x="0.0" y="1296" width="375" height="25"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
@@ -167,8 +167,8 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Join the beta" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Il-5a-5Zp">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Support the team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Il-5a-5Zp">
|
||||||
<rect key="frame" x="30" y="15.5" width="106" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="142.5" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -236,15 +236,51 @@
|
|||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
|
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="GYp-O0-pse" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="444" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="444" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GYp-O0-pse" id="vDG-ZV-xRS">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Disable Idle Timeout" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PCh-Up-aJJ">
|
||||||
|
<rect key="frame" x="30" y="15.5" width="166" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="iQA-wm-5ag">
|
||||||
|
<rect key="frame" x="296" y="10" width="51" height="31"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleNoIdleTimeoutEnabled:" destination="aMk-Xp-UL8" eventType="valueChanged" id="WSl-Jc-g5J"/>
|
||||||
|
</connections>
|
||||||
|
</switch>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="iQA-wm-5ag" secondAttribute="trailing" id="MJ1-HF-Dln"/>
|
||||||
|
<constraint firstItem="PCh-Up-aJJ" firstAttribute="leading" secondItem="vDG-ZV-xRS" secondAttribute="leadingMargin" id="Ocu-jn-RAQ"/>
|
||||||
|
<constraint firstItem="iQA-wm-5ag" firstAttribute="centerY" secondItem="vDG-ZV-xRS" secondAttribute="centerY" id="c6W-bN-VAb"/>
|
||||||
|
<constraint firstItem="PCh-Up-aJJ" firstAttribute="centerY" secondItem="vDG-ZV-xRS" secondAttribute="centerY" id="mL6-LB-cjn"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
|
<integer key="value" value="2"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</tableViewCell>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="495" width="375" height="51"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="amC-sE-8O0" id="GEO-2e-E4k">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="amC-sE-8O0" id="GEO-2e-E4k">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Add to Siri…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Allow Siri To Refresh Apps…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
|
||||||
<rect key="frame" x="30" y="15.5" width="100.5" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="228.5" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -269,7 +305,7 @@
|
|||||||
<tableViewSection headerTitle="" id="eHy-qI-w5w">
|
<tableViewSection headerTitle="" id="eHy-qI-w5w">
|
||||||
<cells>
|
<cells>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="30h-59-88f" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="30h-59-88f" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="535" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="586" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="30h-59-88f" id="7qD-DW-Jls">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="30h-59-88f" id="7qD-DW-Jls">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
@@ -309,7 +345,7 @@
|
|||||||
<tableViewSection headerTitle="" id="J90-vn-u2O">
|
<tableViewSection headerTitle="" id="J90-vn-u2O">
|
||||||
<cells>
|
<cells>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="i4T-2q-jF3" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="i4T-2q-jF3" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="626" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="677" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i4T-2q-jF3" id="VTQ-H4-aCM">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i4T-2q-jF3" id="VTQ-H4-aCM">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
@@ -353,28 +389,28 @@
|
|||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="oHX-oR-nwJ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="oHX-oR-nwJ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="677" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="728" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oHX-oR-nwJ" id="hN4-i5-igu">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oHX-oR-nwJ" id="hN4-i5-igu">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
|
||||||
<rect key="frame" x="30" y="15.5" width="89" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="89" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
|
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
|
||||||
<rect key="frame" x="198" y="15.5" width="147" height="20.5"/>
|
<rect key="frame" x="198" y="15.5" width="147" height="20.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
|
||||||
<rect key="frame" x="129" y="0.0" width="18" height="20.5"/>
|
<rect key="frame" x="129" y="0.0" width="18" height="20.5"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
@@ -397,28 +433,28 @@
|
|||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="0MT-ht-Sit" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="0MT-ht-Sit" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="728" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="779" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0MT-ht-Sit" id="OZp-WM-5H7">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0MT-ht-Sit" id="OZp-WM-5H7">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM">
|
||||||
<rect key="frame" x="30" y="15.5" width="115.5" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="115.5" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
|
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
|
||||||
<rect key="frame" x="206" y="15.5" width="139" height="20.5"/>
|
<rect key="frame" x="206" y="15.5" width="139" height="20.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
|
||||||
<rect key="frame" x="121" y="0.0" width="18" height="20.5"/>
|
<rect key="frame" x="121" y="0.0" width="18" height="20.5"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
@@ -441,19 +477,19 @@
|
|||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="779" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="830" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O5R-Al-lGj" id="CrG-Mr-xQq">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O5R-Al-lGj" id="CrG-Mr-xQq">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK">
|
||||||
<rect key="frame" x="30" y="15.5" width="67.5" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="67.5" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
|
||||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
@@ -481,19 +517,19 @@
|
|||||||
<tableViewSection headerTitle="" id="OMa-EK-hRI">
|
<tableViewSection headerTitle="" id="OMa-EK-hRI">
|
||||||
<cells>
|
<cells>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="870" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="921" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FMZ-as-Ljo" id="JzL-Of-A3T">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FMZ-as-Ljo" id="JzL-Of-A3T">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF">
|
||||||
<rect key="frame" x="30" y="15.5" width="125.5" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="125.5" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
|
||||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
@@ -514,19 +550,19 @@
|
|||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="921" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="972" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
|
||||||
<rect key="frame" x="30" y="15.5" width="187.5" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="187.5" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
|
||||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
@@ -550,19 +586,19 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="rE2-P4-OaE" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="rE2-P4-OaE" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="972" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="1023" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="rE2-P4-OaE" id="qIT-rz-ZUb">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="rE2-P4-OaE" id="qIT-rz-ZUb">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PWC-OG-5jx">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PWC-OG-5jx">
|
||||||
<rect key="frame" x="30" y="15.5" width="119" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="119" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="VfB-c5-5wG">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="VfB-c5-5wG">
|
||||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
@@ -582,23 +618,89 @@
|
|||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
<connections>
|
<connections>
|
||||||
<segue destination="g8a-Rf-zWa" kind="show" identifier="showErrorLog" id="SSW-vL-86I"/>
|
<segue destination="g8a-Rf-zWa" kind="show" identifier="showErrorLog" id="vFC-Id-Ww6"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VrV-qI-zXF" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="1074" width="375" height="51"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VrV-qI-zXF" id="w1r-uY-4pD">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
|
||||||
|
<rect key="frame" x="30" y="15.5" width="183" height="21"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
|
||||||
|
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="wvD-eZ-nQI" firstAttribute="centerY" secondItem="w1r-uY-4pD" secondAttribute="centerY" id="O6Y-Y1-yxv"/>
|
||||||
|
<constraint firstItem="46q-DB-5nc" firstAttribute="centerY" secondItem="w1r-uY-4pD" secondAttribute="centerY" id="ROS-YF-6jb"/>
|
||||||
|
<constraint firstItem="46q-DB-5nc" firstAttribute="leading" secondItem="w1r-uY-4pD" secondAttribute="leadingMargin" id="acd-O8-WTI"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="wvD-eZ-nQI" secondAttribute="trailing" id="taB-EP-QMM"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
|
<integer key="value" value="2"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</tableViewCell>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="eZ3-BT-q4D" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="1125" width="375" height="51"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="eZ3-BT-q4D" id="17m-VV-hzf">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Clear Cache" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IbH-V1-ce3">
|
||||||
|
<rect key="frame" x="30" y="15.5" width="98.5" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="FZe-BJ-fOm">
|
||||||
|
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="FZe-BJ-fOm" firstAttribute="centerY" secondItem="17m-VV-hzf" secondAttribute="centerY" id="bGv-Np-5aO"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="FZe-BJ-fOm" secondAttribute="trailing" id="ccb-JP-Eqi"/>
|
||||||
|
<constraint firstItem="IbH-V1-ce3" firstAttribute="centerY" secondItem="17m-VV-hzf" secondAttribute="centerY" id="iQJ-gN-sRF"/>
|
||||||
|
<constraint firstItem="IbH-V1-ce3" firstAttribute="leading" secondItem="17m-VV-hzf" secondAttribute="leadingMargin" id="m1g-Y6-aT5"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
|
<integer key="value" value="2"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="1023" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="1176" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm">
|
||||||
<rect key="frame" x="30" y="15.5" width="140" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="140" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
|
||||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
@@ -618,28 +720,28 @@
|
|||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="fj2-EJ-Z98" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="1074" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="1227" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fj2-EJ-Z98" id="BcT-Fs-KNg">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="e7s-hL-kv9" id="yjL-Mu-HTk">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Advanced Settings" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OcM-OM-uDE">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
|
||||||
<rect key="frame" x="30" y="15.5" width="154" height="20.5"/>
|
<rect key="frame" x="30" y="15.5" width="135.5" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Pcu-Sy-yfZ">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
|
||||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="trailingMargin" secondItem="Pcu-Sy-yfZ" secondAttribute="trailing" id="CFy-IO-4eb"/>
|
<constraint firstItem="0dh-yd-7i9" firstAttribute="centerY" secondItem="yjL-Mu-HTk" secondAttribute="centerY" id="8OI-PI-weT"/>
|
||||||
<constraint firstItem="OcM-OM-uDE" firstAttribute="centerY" secondItem="BcT-Fs-KNg" secondAttribute="centerY" id="OGl-h4-FPx"/>
|
<constraint firstItem="eds-Dj-36y" firstAttribute="leading" secondItem="yjL-Mu-HTk" secondAttribute="leadingMargin" id="BqG-Ef-xQo"/>
|
||||||
<constraint firstItem="Pcu-Sy-yfZ" firstAttribute="centerY" secondItem="BcT-Fs-KNg" secondAttribute="centerY" id="R7L-4O-lTn"/>
|
<constraint firstAttribute="trailingMargin" secondItem="0dh-yd-7i9" secondAttribute="trailing" id="TFW-nu-jo4"/>
|
||||||
<constraint firstItem="OcM-OM-uDE" firstAttribute="leading" secondItem="BcT-Fs-KNg" secondAttribute="leadingMargin" id="yoh-C6-UC5"/>
|
<constraint firstItem="eds-Dj-36y" firstAttribute="centerY" secondItem="yjL-Mu-HTk" secondAttribute="centerY" id="YiJ-OF-FXE"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</tableViewCellContentView>
|
</tableViewCellContentView>
|
||||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
@@ -656,7 +758,6 @@
|
|||||||
</sections>
|
</sections>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="dataSource" destination="aMk-Xp-UL8" id="c6c-fR-8C4"/>
|
<outlet property="dataSource" destination="aMk-Xp-UL8" id="c6c-fR-8C4"/>
|
||||||
<outlet property="delegate" destination="aMk-Xp-UL8" id="moP-1B-lRq"/>
|
|
||||||
</connections>
|
</connections>
|
||||||
</tableView>
|
</tableView>
|
||||||
<navigationItem key="navigationItem" title="Settings" id="Ddg-UQ-KJ8"/>
|
<navigationItem key="navigationItem" title="Settings" id="Ddg-UQ-KJ8"/>
|
||||||
@@ -665,6 +766,7 @@
|
|||||||
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
|
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
|
||||||
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
|
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
|
||||||
<outlet property="backgroundRefreshSwitch" destination="DPu-zD-Als" id="eiG-Hv-Vko"/>
|
<outlet property="backgroundRefreshSwitch" destination="DPu-zD-Als" id="eiG-Hv-Vko"/>
|
||||||
|
<outlet property="noIdleTimeoutSwitch" destination="iQA-wm-5ag" id="jHC-js-q0Y"/>
|
||||||
<outlet property="versionLabel" destination="bUR-rp-Nw2" id="85I-5R-hqz"/>
|
<outlet property="versionLabel" destination="bUR-rp-Nw2" id="85I-5R-hqz"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tableViewController>
|
</tableViewController>
|
||||||
@@ -680,7 +782,7 @@
|
|||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
|
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Jtn-cs-Tvp" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Jtn-cs-Tvp" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<color key="barTintColor" name="SettingsBackground"/>
|
<color key="barTintColor" name="SettingsBackground"/>
|
||||||
@@ -781,7 +883,7 @@
|
|||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" layoutMarginsFollowReadableWidth="YES" contentInsetAdjustmentBehavior="never" indicatorStyle="white" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oQQ-pR-oKc">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" layoutMarginsFollowReadableWidth="YES" contentInsetAdjustmentBehavior="never" indicatorStyle="white" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oQQ-pR-oKc">
|
||||||
<rect key="frame" x="0.0" y="44" width="375" height="574"/>
|
<rect key="frame" x="0.0" y="64" width="375" height="554"/>
|
||||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||||
<string key="text">Jay Freeman (ldid)
|
<string key="text">Jay Freeman (ldid)
|
||||||
Copyright (C) 2007-2012 Jay Freeman (saurik)
|
Copyright (C) 2007-2012 Jay Freeman (saurik)
|
||||||
@@ -884,7 +986,7 @@ Settings by i cons from the Noun Project</string>
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1697" y="313"/>
|
<point key="canvasLocation" x="1697" y="313"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Patreon-->
|
<!--Support us-->
|
||||||
<scene sceneID="Lnh-9P-HnL">
|
<scene sceneID="Lnh-9P-HnL">
|
||||||
<objects>
|
<objects>
|
||||||
<collectionViewController id="dp8-8j-vt9" customClass="PatreonViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
<collectionViewController id="dp8-8j-vt9" customClass="PatreonViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
@@ -931,7 +1033,7 @@ Settings by i cons from the Noun Project</string>
|
|||||||
<outlet property="delegate" destination="dp8-8j-vt9" id="790-Kr-6l7"/>
|
<outlet property="delegate" destination="dp8-8j-vt9" id="790-Kr-6l7"/>
|
||||||
</connections>
|
</connections>
|
||||||
</collectionView>
|
</collectionView>
|
||||||
<navigationItem key="navigationItem" title="Patreon" largeTitleDisplayMode="always" id="uUV-1f-xEq"/>
|
<navigationItem key="navigationItem" title="Support us" largeTitleDisplayMode="always" id="uUV-1f-xEq"/>
|
||||||
</collectionViewController>
|
</collectionViewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="qq3-Hj-S9f" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="qq3-Hj-S9f" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
@@ -1015,7 +1117,7 @@ Settings by i cons from the Noun Project</string>
|
|||||||
</textView>
|
</textView>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5">
|
<button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5" customClass="ErrorLogMenuButton">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/>
|
||||||
<accessibility key="accessibilityConfiguration">
|
<accessibility key="accessibilityConfiguration">
|
||||||
<bool key="isElement" value="NO"/>
|
<bool key="isElement" value="NO"/>
|
||||||
@@ -1064,11 +1166,73 @@ Settings by i cons from the Noun Project</string>
|
|||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
</rightBarButtonItems>
|
</rightBarButtonItems>
|
||||||
</navigationItem>
|
</navigationItem>
|
||||||
|
<connections>
|
||||||
|
<segue destination="7gm-d1-zWK" kind="presentation" identifier="showErrorDetails" id="9vz-y6-evp"/>
|
||||||
|
</connections>
|
||||||
</tableViewController>
|
</tableViewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1697" y="1774"/>
|
<point key="canvasLocation" x="1697" y="1774"/>
|
||||||
</scene>
|
</scene>
|
||||||
|
<!--Error Details View Controller-->
|
||||||
|
<scene sceneID="XNO-Yg-I7t">
|
||||||
|
<objects>
|
||||||
|
<viewController id="xB2-Se-VVg" customClass="ErrorDetailsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="eBQ-se-VIy">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="ctd-NB-4ov">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
<color key="textColor" systemColor="labelColor"/>
|
||||||
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
|
</textView>
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="Nm8-69-Ngi"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="ctd-NB-4ov" firstAttribute="leading" secondItem="eBQ-se-VIy" secondAttribute="leading" id="Cv1-Te-gBH"/>
|
||||||
|
<constraint firstItem="ctd-NB-4ov" firstAttribute="top" secondItem="eBQ-se-VIy" secondAttribute="top" id="HRY-Rg-iMI"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="ctd-NB-4ov" secondAttribute="trailing" id="Lc1-K7-iuq"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="ctd-NB-4ov" secondAttribute="bottom" id="zCz-Cy-Y5z"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<navigationItem key="navigationItem" id="XpE-V9-EaY">
|
||||||
|
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="rnr-dX-4Ev">
|
||||||
|
<connections>
|
||||||
|
<segue destination="ZSp-1n-UJ9" kind="unwind" unwindAction="unwindFromErrorDetails:" id="TFu-zD-QyF"/>
|
||||||
|
</connections>
|
||||||
|
</barButtonItem>
|
||||||
|
</navigationItem>
|
||||||
|
<connections>
|
||||||
|
<outlet property="textView" destination="ctd-NB-4ov" id="x2C-9R-Xz1"/>
|
||||||
|
</connections>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="8AM-Vx-XTN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
<exit id="ZSp-1n-UJ9" userLabel="Exit" sceneMemberID="exit"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="3389.5999999999999" y="1772.5637181409297"/>
|
||||||
|
</scene>
|
||||||
|
<!--Navigation Controller-->
|
||||||
|
<scene sceneID="4LJ-Od-dCK">
|
||||||
|
<objects>
|
||||||
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="7gm-d1-zWK" sceneMemberID="viewController">
|
||||||
|
<toolbarItems/>
|
||||||
|
<navigationBar key="navigationBar" contentMode="scaleToFill" id="dI0-sh-yGf">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</navigationBar>
|
||||||
|
<nil name="viewControllers"/>
|
||||||
|
<connections>
|
||||||
|
<segue destination="xB2-Se-VVg" kind="relationship" relationship="rootViewController" id="RpP-UM-JfJ"/>
|
||||||
|
</connections>
|
||||||
|
</navigationController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="OXW-bf-HIj" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="2554" y="1773"/>
|
||||||
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="Next" width="18" height="18"/>
|
<image name="Next" width="18" height="18"/>
|
||||||
@@ -1081,7 +1245,10 @@ Settings by i cons from the Noun Project</string>
|
|||||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<systemColor name="labelColor">
|
<systemColor name="labelColor">
|
||||||
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import MessageUI
|
import MessageUI
|
||||||
import Intents
|
import Intents
|
||||||
@@ -30,13 +31,14 @@ extension SettingsViewController
|
|||||||
fileprivate enum AppRefreshRow: Int, CaseIterable
|
fileprivate enum AppRefreshRow: Int, CaseIterable
|
||||||
{
|
{
|
||||||
case backgroundRefresh
|
case backgroundRefresh
|
||||||
|
case noIdleTimeout
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
case addToSiri
|
case addToSiri
|
||||||
|
|
||||||
static var allCases: [AppRefreshRow] {
|
static var allCases: [AppRefreshRow] {
|
||||||
guard #available(iOS 14, *) else { return [.backgroundRefresh] }
|
guard #available(iOS 14, *) else { return [.backgroundRefresh, .noIdleTimeout] }
|
||||||
return [.backgroundRefresh, .addToSiri]
|
return [.backgroundRefresh, .noIdleTimeout, .addToSiri]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +55,12 @@ extension SettingsViewController
|
|||||||
case sendFeedback
|
case sendFeedback
|
||||||
case refreshAttempts
|
case refreshAttempts
|
||||||
case errorLog
|
case errorLog
|
||||||
|
case refreshSideJITServer
|
||||||
|
case clearCache
|
||||||
case resetPairingFile
|
case resetPairingFile
|
||||||
|
case anisetteServers
|
||||||
case advancedSettings
|
case advancedSettings
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +78,9 @@ final class SettingsViewController: UITableViewController
|
|||||||
@IBOutlet private var accountTypeLabel: UILabel!
|
@IBOutlet private var accountTypeLabel: UILabel!
|
||||||
|
|
||||||
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
|
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
|
||||||
|
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
|
||||||
|
|
||||||
|
@IBOutlet private var refreshSideJITServer: UILabel!
|
||||||
|
|
||||||
@IBOutlet private var versionLabel: UILabel!
|
@IBOutlet private var versionLabel: UILabel!
|
||||||
|
|
||||||
@@ -84,6 +93,7 @@ final class SettingsViewController: UITableViewController
|
|||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad()
|
override func viewDidLoad()
|
||||||
@@ -101,16 +111,36 @@ final class SettingsViewController: UITableViewController
|
|||||||
debugModeGestureRecognizer.numberOfTouchesRequired = 3
|
debugModeGestureRecognizer.numberOfTouchesRequired = 3
|
||||||
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
|
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
|
||||||
|
|
||||||
|
var versionString: String = ""
|
||||||
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
||||||
{
|
{
|
||||||
self.versionLabel.text = NSLocalizedString(String(format: "SideStore %@", version), comment: "SideStore Version")
|
versionString += "SideStore \(version)"
|
||||||
|
if let xcode = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String {
|
||||||
|
versionString += " - Xcode \(xcode) - "
|
||||||
|
if let build = Bundle.main.object(forInfoDictionaryKey: "DTXcodeBuild") as? String {
|
||||||
|
versionString += "\(build)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String {
|
||||||
|
let pair_test = pairing == "<insert pairing file here>"
|
||||||
|
if !pair_test {
|
||||||
|
versionString += " - \(!pair_test)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
self.versionLabel.text = NSLocalizedString("SideStore", comment: "")
|
versionString += "SideStore\t"
|
||||||
}
|
}
|
||||||
|
versionString += "\n\(Bundle.Info.appbundleIdentifier)"
|
||||||
|
|
||||||
|
self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version")
|
||||||
|
|
||||||
self.tableView.contentInset.bottom = 20
|
self.versionLabel.numberOfLines = 0
|
||||||
|
self.versionLabel.lineBreakMode = .byWordWrapping
|
||||||
|
self.versionLabel.setNeedsUpdateConstraints()
|
||||||
|
|
||||||
|
self.tableView.contentInset.bottom = 40
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@@ -127,6 +157,18 @@ final class SettingsViewController: UITableViewController
|
|||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
|
if segue.identifier == "anisetteServers" {
|
||||||
|
let controller = UIHostingController(rootView: AnisetteServers(selected: UserDefaults.standard.menuAnisetteURL, errorCallback: {
|
||||||
|
ToastView(text: "Cleared adi.pb!", detailText: "You will need to log back into Apple ID in SideStore.").show(in: self)
|
||||||
|
}))
|
||||||
|
self.show(controller, sender: nil)
|
||||||
|
} else {
|
||||||
|
super.prepare(for: segue, sender: sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension SettingsViewController
|
private extension SettingsViewController
|
||||||
@@ -147,6 +189,7 @@ private extension SettingsViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
|
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
|
||||||
|
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
|
||||||
|
|
||||||
if self.isViewLoaded
|
if self.isViewLoaded
|
||||||
{
|
{
|
||||||
@@ -177,11 +220,11 @@ private extension SettingsViewController
|
|||||||
case .patreon:
|
case .patreon:
|
||||||
if isHeader
|
if isHeader
|
||||||
{
|
{
|
||||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("PATREON", comment: "")
|
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("SUPPORT US", comment: "")
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Support the SideStore Team by becoming a patron!", comment: "")
|
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Support the SideStore Team by following our socials or becoming a patron!", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
case .account:
|
case .account:
|
||||||
@@ -198,7 +241,7 @@ private extension SettingsViewController
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Enable Background Refresh to automatically refresh apps in the background when connected to Wi-Fi.", comment: "")
|
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Enable Background Refresh to automatically refresh apps in the background when connected to Wi-Fi. \n\nDisable the Idle Timeout toggle to allow SideStore to not let your device go to sleep during a refresh or install of any apps.", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
case .instructions:
|
case .instructions:
|
||||||
@@ -279,6 +322,11 @@ private extension SettingsViewController
|
|||||||
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn
|
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IBAction func toggleNoIdleTimeoutEnabled(_ sender: UISwitch)
|
||||||
|
{
|
||||||
|
UserDefaults.standard.isIdleTimeoutDisableEnabled = sender.isOn
|
||||||
|
}
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
@IBAction func addRefreshAppsShortcut()
|
@IBAction func addRefreshAppsShortcut()
|
||||||
{
|
{
|
||||||
@@ -290,6 +338,39 @@ private extension SettingsViewController
|
|||||||
self.present(viewController, animated: true, completion: nil)
|
self.present(viewController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearCache()
|
||||||
|
{
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear SideStore's cache?", comment: ""),
|
||||||
|
message: NSLocalizedString("This will remove all temporary files as well as backups for uninstalled apps.", comment: ""),
|
||||||
|
preferredStyle: .actionSheet)
|
||||||
|
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { [weak self] _ in
|
||||||
|
self?.tableView.indexPathForSelectedRow.map { self?.tableView.deselectRow(at: $0, animated: true) }
|
||||||
|
})
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Clear Cache", comment: ""), style: .destructive) { [weak self] _ in
|
||||||
|
AppManager.shared.clearAppCache { result in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.tableView.indexPathForSelectedRow.map { self?.tableView.deselectRow(at: $0, animated: true) }
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success: break
|
||||||
|
case .failure(let error):
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("Unable to Clear Cache", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
alertController.addAction(.ok)
|
||||||
|
self?.present(alertController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if let popoverController = alertController.popoverPresentationController {
|
||||||
|
popoverController.sourceView = self.view
|
||||||
|
popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.present(alertController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
@IBAction func handleDebugModeGesture(_ gestureRecognizer: UISwipeGestureRecognizer)
|
@IBAction func handleDebugModeGesture(_ gestureRecognizer: UISwipeGestureRecognizer)
|
||||||
{
|
{
|
||||||
self.debugGestureCounter += 1
|
self.debugGestureCounter += 1
|
||||||
@@ -344,6 +425,15 @@ private extension SettingsViewController
|
|||||||
self.performSegue(withIdentifier: "showPatreon", sender: nil)
|
self.performSegue(withIdentifier: "showPatreon", sender: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openErrorLog(_: Notification) {
|
||||||
|
guard self.presentedViewController == nil else { return }
|
||||||
|
|
||||||
|
self.navigationController?.popViewController(animated: false)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
self.performSegue(withIdentifier: "showErrorLog", sender: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SettingsViewController
|
extension SettingsViewController
|
||||||
@@ -385,6 +475,17 @@ extension SettingsViewController
|
|||||||
cell.style = .single
|
cell.style = .single
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if AppRefreshRow.AllCases().count == 1
|
||||||
|
{
|
||||||
|
if let cell = cell as? InsetGroupTableViewCell,
|
||||||
|
indexPath.section == Section.appRefresh.rawValue,
|
||||||
|
indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
|
||||||
|
{
|
||||||
|
cell.style = .single
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +541,7 @@ extension SettingsViewController
|
|||||||
switch section
|
switch section
|
||||||
{
|
{
|
||||||
case .signIn where self.activeTeam != nil: return 1.0
|
case .signIn where self.activeTeam != nil: return 1.0
|
||||||
case .account where self.activeTeam == nil: return 1.0
|
case .account where self.activeTeam == nil: return 1.0
|
||||||
case .signIn, .patreon, .appRefresh:
|
case .signIn, .patreon, .appRefresh:
|
||||||
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
|
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
|
||||||
return height
|
return height
|
||||||
@@ -464,11 +565,13 @@ extension SettingsViewController
|
|||||||
switch row
|
switch row
|
||||||
{
|
{
|
||||||
case .backgroundRefresh: break
|
case .backgroundRefresh: break
|
||||||
|
case .noIdleTimeout: break
|
||||||
case .addToSiri:
|
case .addToSiri:
|
||||||
guard #available(iOS 14, *) else { return }
|
guard #available(iOS 14, *) else { return }
|
||||||
self.addRefreshAppsShortcut()
|
self.addRefreshAppsShortcut()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
case .credits:
|
case .credits:
|
||||||
let row = CreditsRow.allCases[indexPath.row]
|
let row = CreditsRow.allCases[indexPath.row]
|
||||||
switch row
|
switch row
|
||||||
@@ -484,31 +587,158 @@ extension SettingsViewController
|
|||||||
switch row
|
switch row
|
||||||
{
|
{
|
||||||
case .sendFeedback:
|
case .sendFeedback:
|
||||||
if MFMailComposeViewController.canSendMail()
|
let alertController = UIAlertController(title: "Send Feedback", message: "Choose a method to send feedback:", preferredStyle: .actionSheet)
|
||||||
{
|
|
||||||
let mailViewController = MFMailComposeViewController()
|
// Option 1: GitHub
|
||||||
mailViewController.mailComposeDelegate = self
|
alertController.addAction(UIAlertAction(title: "GitHub", style: .default) { _ in
|
||||||
mailViewController.setToRecipients(["support@sidestore.io"])
|
if let githubURL = URL(string: "https://github.com/SideStore/SideStore/issues") {
|
||||||
|
let safariViewController = SFSafariViewController(url: githubURL)
|
||||||
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
safariViewController.preferredControlTintColor = .altPrimary
|
||||||
{
|
self.present(safariViewController, animated: true, completion: nil)
|
||||||
mailViewController.setSubject("SideStore Beta \(version) Feedback")
|
|
||||||
}
|
}
|
||||||
else
|
})
|
||||||
{
|
|
||||||
mailViewController.setSubject("SideStore Beta Feedback")
|
// Option 2: Discord
|
||||||
|
alertController.addAction(UIAlertAction(title: "Discord", style: .default) { _ in
|
||||||
|
if let discordURL = URL(string: "https://discord.gg/sidestore-949183273383395328") {
|
||||||
|
let safariViewController = SFSafariViewController(url: discordURL)
|
||||||
|
safariViewController.preferredControlTintColor = .altPrimary
|
||||||
|
self.present(safariViewController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Option 3: Mail
|
||||||
|
// alertController.addAction(UIAlertAction(title: "Send Email", style: .default) { _ in
|
||||||
|
// if MFMailComposeViewController.canSendMail() {
|
||||||
|
// let mailViewController = MFMailComposeViewController()
|
||||||
|
// mailViewController.mailComposeDelegate = self
|
||||||
|
// mailViewController.setToRecipients(["support@sidestore.io"])
|
||||||
|
//
|
||||||
|
// if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
|
||||||
|
// mailViewController.setSubject("SideStore Beta \(version) Feedback")
|
||||||
|
// } else {
|
||||||
|
// mailViewController.setSubject("SideStore Beta Feedback")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// self.present(mailViewController, animated: true, completion: nil)
|
||||||
|
// } else {
|
||||||
|
// let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil)
|
||||||
|
// toastView.show(in: self)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// Cancel action
|
||||||
|
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||||
|
|
||||||
|
// For iPad: Set the source view if presenting on iPad to avoid crashes
|
||||||
|
if let popoverController = alertController.popoverPresentationController {
|
||||||
|
popoverController.sourceView = self.view
|
||||||
|
popoverController.sourceRect = self.view.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present the action sheet
|
||||||
|
self.present(alertController, animated: true, completion: nil)
|
||||||
|
|
||||||
|
case .refreshSideJITServer:
|
||||||
|
if #available(iOS 17, *) {
|
||||||
|
|
||||||
|
let alertController = UIAlertController(
|
||||||
|
title: NSLocalizedString("SideJITServer", comment: ""),
|
||||||
|
message: NSLocalizedString("Settings for SideJITServer", comment: ""),
|
||||||
|
preferredStyle: UIAlertController.Style.actionSheet)
|
||||||
|
|
||||||
|
|
||||||
|
if UserDefaults.standard.sidejitenable {
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Disable", comment: ""), style: .default){ _ in
|
||||||
|
UserDefaults.standard.sidejitenable = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Enable", comment: ""), style: .default){ _ in
|
||||||
|
UserDefaults.standard.sidejitenable = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
self.present(mailViewController, animated: true, completion: nil)
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Server Address", comment: ""), style: .default){ _ in
|
||||||
}
|
let alertController1 = UIAlertController(title: "SideJITServer Address", message: "Please Enter the SideJITServer Address Below. (this is not needed if SideJITServer has already been detected)", preferredStyle: .alert)
|
||||||
else
|
|
||||||
{
|
|
||||||
let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil)
|
alertController1.addTextField { textField in
|
||||||
toastView.show(in: self)
|
textField.placeholder = "SideJITServer Address"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
|
||||||
|
alertController1.addAction(cancelAction)
|
||||||
|
|
||||||
|
|
||||||
|
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
||||||
|
if let text = alertController1.textFields?.first?.text {
|
||||||
|
UserDefaults.standard.textInputSideJITServerurl = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alertController1.addAction(okAction)
|
||||||
|
|
||||||
|
// Present the alert controller
|
||||||
|
self.present(alertController1, animated: true)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh", comment: ""), style: .destructive){ _ in
|
||||||
|
if UserDefaults.standard.sidejitenable {
|
||||||
|
var SJSURL = ""
|
||||||
|
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||||
|
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||||
|
} else {
|
||||||
|
SJSURL = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let url = URL(string: SJSURL + "/re/")!
|
||||||
|
|
||||||
|
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
||||||
|
if let error = error {
|
||||||
|
print("Error: \(error)")
|
||||||
|
} else {
|
||||||
|
// Do nothing with data or response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
|
||||||
|
alertController.addAction(cancelAction)
|
||||||
|
//Fix crash on iPad
|
||||||
|
alertController.popoverPresentationController?.sourceView = self.tableView
|
||||||
|
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
|
||||||
|
self.present(alertController, animated: true)
|
||||||
|
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
} else {
|
||||||
|
let alertController = UIAlertController(
|
||||||
|
title: NSLocalizedString("You are not on iOS 17+ This will not work", comment: ""),
|
||||||
|
message: NSLocalizedString("This is meant for 'SideJITServer' and it only works on iOS 17+ ", comment: ""),
|
||||||
|
preferredStyle: UIAlertController.Style.actionSheet)
|
||||||
|
|
||||||
|
alertController.addAction(.cancel)
|
||||||
|
//Fix crash on iPad
|
||||||
|
alertController.popoverPresentationController?.sourceView = self.tableView
|
||||||
|
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
|
||||||
|
self.present(alertController, animated: true)
|
||||||
|
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
case .clearCache: self.clearCache()
|
||||||
|
|
||||||
case .resetPairingFile:
|
case .resetPairingFile:
|
||||||
|
|
||||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||||
|
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
|
|
||||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||||
let alertController = UIAlertController(
|
let alertController = UIAlertController(
|
||||||
title: NSLocalizedString("Are you sure to reset the pairing file?", comment: ""),
|
title: NSLocalizedString("Are you sure to reset the pairing file?", comment: ""),
|
||||||
@@ -517,11 +747,12 @@ extension SettingsViewController
|
|||||||
|
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete and Reset", comment: ""), style: .destructive){ _ in
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete and Reset", comment: ""), style: .destructive){ _ in
|
||||||
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
|
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
|
||||||
|
UserDefaults.standard.isPairingReset = true
|
||||||
try? fm.removeItem(atPath: documentsPath.path)
|
try? fm.removeItem(atPath: documentsPath.path)
|
||||||
NSLog("Pairing File Reseted")
|
NSLog("Pairing File Reseted")
|
||||||
}
|
}
|
||||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||||
let dialogMessage = UIAlertController(title: NSLocalizedString("Pairing File Reseted", comment: ""), message: NSLocalizedString("Please restart SideStore", comment: ""), preferredStyle: .alert)
|
let dialogMessage = UIAlertController(title: NSLocalizedString("Pairing File Reset", comment: ""), message: NSLocalizedString("Please restart SideStore", comment: ""), preferredStyle: .alert)
|
||||||
self.present(dialogMessage, animated: true, completion: nil)
|
self.present(dialogMessage, animated: true, completion: nil)
|
||||||
})
|
})
|
||||||
alertController.addAction(.cancel)
|
alertController.addAction(.cancel)
|
||||||
@@ -530,6 +761,12 @@ extension SettingsViewController
|
|||||||
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
|
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
|
||||||
self.present(alertController, animated: true)
|
self.present(alertController, animated: true)
|
||||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
||||||
|
case .anisetteServers:
|
||||||
|
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: UIHostingController(rootView: AnisetteServers(selected: "", errorCallback: {
|
||||||
|
ToastView(text: "Reset adi.pb", detailText: "Buh").show(in: self)
|
||||||
|
}))), sender: nil)
|
||||||
|
// self.performSegue(withIdentifier: "anisetteServers", sender: nil)
|
||||||
case .advancedSettings:
|
case .advancedSettings:
|
||||||
// Create the URL that deep links to your app's custom settings.
|
// Create the URL that deep links to your app's custom settings.
|
||||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||||
@@ -539,6 +776,7 @@ extension SettingsViewController
|
|||||||
ELOG("UIApplication.openSettingsURLString invalid")
|
ELOG("UIApplication.openSettingsURLString invalid")
|
||||||
}
|
}
|
||||||
case .refreshAttempts, .errorLog: break
|
case .refreshAttempts, .errorLog: break
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default: break
|
default: break
|
||||||
|
|||||||
@@ -12,17 +12,22 @@ import CoreData
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
struct SourceError: LocalizedError
|
struct SourceError: ALTLocalizedError
|
||||||
{
|
{
|
||||||
enum Code
|
enum Code: Int, ALTErrorCode
|
||||||
{
|
{
|
||||||
|
typealias Error = SourceError
|
||||||
|
|
||||||
case unsupported
|
case unsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
var code: Code
|
var code: Code
|
||||||
|
var errorTitle: String?
|
||||||
|
var errorFailure: String?
|
||||||
|
|
||||||
@Managed var source: Source
|
@Managed var source: Source
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorFailureReason: String {
|
||||||
switch self.code
|
switch self.code
|
||||||
{
|
{
|
||||||
case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of SideStore.", comment: ""), self.$source.name)
|
case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of SideStore.", comment: ""), self.$source.name)
|
||||||
@@ -197,7 +202,7 @@ private extension SourcesViewController
|
|||||||
{
|
{
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("Add Source", comment: ""), message: nil, preferredStyle: .alert)
|
let alertController = UIAlertController(title: NSLocalizedString("Add Source", comment: ""), message: nil, preferredStyle: .alert)
|
||||||
alertController.addTextField { (textField) in
|
alertController.addTextField { (textField) in
|
||||||
textField.placeholder = "https://apps.altstore.io"
|
textField.placeholder = "https://apps.sidestore.io"
|
||||||
textField.textContentType = .URL
|
textField.textContentType = .URL
|
||||||
}
|
}
|
||||||
alertController.addAction(.cancel)
|
alertController.addAction(.cancel)
|
||||||
@@ -545,19 +550,19 @@ extension SourcesViewController: UICollectionViewDelegateFlowLayout
|
|||||||
footerView.textView.delegate = self
|
footerView.textView.delegate = self
|
||||||
|
|
||||||
let attributedText = NSMutableAttributedString(
|
let attributedText = NSMutableAttributedString(
|
||||||
string: NSLocalizedString("SideStore has reviewed these sources to make sure they meet our safety standards.\n\nSupport for untrusted sources is currently in beta, but you can help test them out by", comment: ""),
|
string: NSLocalizedString("SideStore has reviewed these sources to make sure they meet our safety standards.", comment: ""),
|
||||||
attributes: [.font: font, .foregroundColor: UIColor.gray]
|
attributes: [.font: font, .foregroundColor: UIColor.gray]
|
||||||
)
|
)
|
||||||
attributedText.mutableString.append(" ")
|
//attributedText.mutableString.append(" ")
|
||||||
|
|
||||||
let boldedFont = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize)
|
//let boldedFont = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize)
|
||||||
let openPatreonURL = URL(string: "https://SideStore.io/patreon")!
|
//let openPatreonURL = URL(string: "https://SideStore.io/")!
|
||||||
|
|
||||||
let joinPatreonText = NSAttributedString(
|
// let joinPatreonText = NSAttributedString(
|
||||||
string: NSLocalizedString("joining our Patreon.", comment: ""),
|
// string: NSLocalizedString("", comment: ""),
|
||||||
attributes: [.font: boldedFont, .link: openPatreonURL, .underlineColor: UIColor.clear]
|
// attributes: [.font: boldedFont, .link: openPatreonURL, .underlineColor: UIColor.clear]
|
||||||
)
|
//)
|
||||||
attributedText.append(joinPatreonText)
|
//attributedText.append(joinPatreonText)
|
||||||
|
|
||||||
footerView.textView.attributedText = attributedText
|
footerView.textView.attributedText = attributedText
|
||||||
footerView.textView.textAlignment = .natural
|
footerView.textView.textAlignment = .natural
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ final class TabBarController: UITabBarController
|
|||||||
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.presentSources(_:)), name: AppDelegate.addSourceDeepLinkNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.presentSources(_:)), name: AppDelegate.addSourceDeepLinkNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool)
|
override func viewDidAppear(_ animated: Bool)
|
||||||
@@ -141,4 +142,7 @@ private extension TabBarController
|
|||||||
{
|
{
|
||||||
self.selectedIndex = Tab.myApps.rawValue
|
self.selectedIndex = Tab.myApps.rawValue
|
||||||
}
|
}
|
||||||
|
@objc func openErrorLog(_: Notification){
|
||||||
|
self.selectedIndex = Tab.settings.rawValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,23 +10,27 @@ import Foundation
|
|||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
@propertyWrapper @dynamicMemberLookup
|
@propertyWrapper @dynamicMemberLookup
|
||||||
struct Managed<ManagedObject: NSManagedObject>
|
struct Managed<ManagedObject>
|
||||||
{
|
{
|
||||||
var wrappedValue: ManagedObject {
|
var wrappedValue: ManagedObject {
|
||||||
didSet {
|
didSet {
|
||||||
self.managedObjectContext = self.wrappedValue.managedObjectContext
|
self.managedObjectContext = self.managedObject?.managedObjectContext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var managedObjectContext: NSManagedObjectContext?
|
|
||||||
|
|
||||||
var projectedValue: Managed<ManagedObject> {
|
var projectedValue: Managed<ManagedObject> {
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var managedObjectContext: NSManagedObjectContext?
|
||||||
|
private var managedObject: NSManagedObject? {
|
||||||
|
return self.wrappedValue as? NSManagedObject
|
||||||
|
}
|
||||||
|
|
||||||
init(wrappedValue: ManagedObject)
|
init(wrappedValue: ManagedObject)
|
||||||
{
|
{
|
||||||
self.wrappedValue = wrappedValue
|
self.wrappedValue = wrappedValue
|
||||||
self.managedObjectContext = wrappedValue.managedObjectContext
|
self.managedObjectContext = self.managedObject?.managedObjectContext
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript<T>(dynamicMember keyPath: KeyPath<ManagedObject, T>) -> T
|
subscript<T>(dynamicMember keyPath: KeyPath<ManagedObject, T>) -> T
|
||||||
@@ -46,4 +50,18 @@ struct Managed<ManagedObject: NSManagedObject>
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optionals
|
||||||
|
subscript<Wrapped, T>(dynamicMember keyPath: KeyPath<Wrapped, T>) -> T? where ManagedObject == Optional<Wrapped> {
|
||||||
|
var result: T?
|
||||||
|
|
||||||
|
if let context = self.managedObjectContext {
|
||||||
|
context.performAndWait {
|
||||||
|
result = self.wrappedValue?[keyPath: keyPath] as? T
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = self.wrappedValue?[keyPath: keyPath] as? T
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
|
|||||||
// Shared
|
// Shared
|
||||||
#import <AltStoreCore/ALTConstants.h>
|
#import <AltStoreCore/ALTConstants.h>
|
||||||
#import <AltStoreCore/ALTConnection.h>
|
#import <AltStoreCore/ALTConnection.h>
|
||||||
|
#import <AltStoreCore/ALTWrappedError.h>
|
||||||
#import <AltStoreCore/NSError+ALTServerError.h>
|
#import <AltStoreCore/NSError+ALTServerError.h>
|
||||||
#import <AltStoreCore/CFNotificationName+AltStore.h>
|
#import <AltStoreCore/CFNotificationName+AltStore.h>
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ public class Keychain
|
|||||||
@KeychainItem(key: "patreonAccountID")
|
@KeychainItem(key: "patreonAccountID")
|
||||||
public var patreonAccountID: String?
|
public var patreonAccountID: String?
|
||||||
|
|
||||||
|
@KeychainItem(key: "identifier")
|
||||||
|
public var identifier: String?
|
||||||
|
|
||||||
|
@KeychainItem(key: "adiPb")
|
||||||
|
public var adiPb: String?
|
||||||
|
|
||||||
private init()
|
private init()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// OperatingSystemVersion+Comparable.swift
|
||||||
|
// AltStoreCore
|
||||||
|
//
|
||||||
|
// Created by nythepegasus on 5/9/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension OperatingSystemVersion: Comparable {
|
||||||
|
public static func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
|
||||||
|
return lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func <(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
|
||||||
|
return lhs.stringValue.compare(rhs.stringValue, options: .numeric) == .orderedAscending
|
||||||
|
}
|
||||||
|
}
|
||||||
14
AltStoreCore/Extensions/String+SideStore.swift
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// String+SideStore.swift
|
||||||
|
// AltStoreCore
|
||||||
|
//
|
||||||
|
// Created by nythepegasus on 5/9/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension String {
|
||||||
|
init(formatted: String, comment: String? = nil, _ args: String...) {
|
||||||
|
self.init(format: NSLocalizedString(formatted, comment: comment ?? ""), args)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,11 +22,17 @@ public extension UserDefaults
|
|||||||
@NSManaged var firstLaunch: Date?
|
@NSManaged var firstLaunch: Date?
|
||||||
@NSManaged var requiresAppGroupMigration: Bool
|
@NSManaged var requiresAppGroupMigration: Bool
|
||||||
@NSManaged var textServer: Bool
|
@NSManaged var textServer: Bool
|
||||||
|
@NSManaged var sidejitenable: Bool
|
||||||
|
@NSManaged var textInputSideJITServerurl: String?
|
||||||
@NSManaged var textInputAnisetteURL: String?
|
@NSManaged var textInputAnisetteURL: String?
|
||||||
@NSManaged var customAnisetteURL: String?
|
@NSManaged var customAnisetteURL: String?
|
||||||
|
@NSManaged var menuAnisetteURL: String
|
||||||
|
@NSManaged var menuAnisetteList: String
|
||||||
@NSManaged var preferredServerID: String?
|
@NSManaged var preferredServerID: String?
|
||||||
|
|
||||||
@NSManaged var isBackgroundRefreshEnabled: Bool
|
@NSManaged var isBackgroundRefreshEnabled: Bool
|
||||||
|
@NSManaged var isIdleTimeoutDisableEnabled: Bool
|
||||||
|
@NSManaged var isPairingReset: Bool
|
||||||
@NSManaged var isDebugModeEnabled: Bool
|
@NSManaged var isDebugModeEnabled: Bool
|
||||||
@NSManaged var presentedLaunchReminderNotification: Bool
|
@NSManaged var presentedLaunchReminderNotification: Bool
|
||||||
|
|
||||||
@@ -42,6 +48,7 @@ public extension UserDefaults
|
|||||||
@NSManaged var patronsRefreshID: String?
|
@NSManaged var patronsRefreshID: String?
|
||||||
|
|
||||||
@NSManaged var trustedSourceIDs: [String]?
|
@NSManaged var trustedSourceIDs: [String]?
|
||||||
|
@NSManaged var trustedServerURL: String?
|
||||||
|
|
||||||
var activeAppsLimit: Int? {
|
var activeAppsLimit: Int? {
|
||||||
get {
|
get {
|
||||||
@@ -71,11 +78,14 @@ public extension UserDefaults
|
|||||||
|
|
||||||
let defaults = [
|
let defaults = [
|
||||||
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
||||||
|
#keyPath(UserDefaults.isIdleTimeoutDisableEnabled): true,
|
||||||
|
#keyPath(UserDefaults.isPairingReset): true,
|
||||||
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
|
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
|
||||||
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,
|
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,
|
||||||
#keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing,
|
#keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing,
|
||||||
#keyPath(UserDefaults.requiresAppGroupMigration): true
|
#keyPath(UserDefaults.requiresAppGroupMigration): true,
|
||||||
]
|
#keyPath(UserDefaults.menuAnisetteURL): "https://ani.sidestore.io"
|
||||||
|
] as [String : Any]
|
||||||
|
|
||||||
UserDefaults.standard.register(defaults: defaults)
|
UserDefaults.standard.register(defaults: defaults)
|
||||||
UserDefaults.shared.register(defaults: defaults)
|
UserDefaults.shared.register(defaults: defaults)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
|||||||
|
|
||||||
/* Relationships */
|
/* Relationships */
|
||||||
@NSManaged public private(set) var app: StoreApp?
|
@NSManaged public private(set) var app: StoreApp?
|
||||||
@NSManaged public private(set) var latestVersionApp: StoreApp?
|
@NSManaged @objc(latestVersionApp) public internal(set) var latestSupportedVersionApp: StoreApp?
|
||||||
|
|
||||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
{
|
{
|
||||||
@@ -54,6 +54,8 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
|||||||
case localizedDescription
|
case localizedDescription
|
||||||
case downloadURL
|
case downloadURL
|
||||||
case size
|
case size
|
||||||
|
case minOSVersion
|
||||||
|
case maxOSVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws
|
public required init(from decoder: Decoder) throws
|
||||||
@@ -72,6 +74,9 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
|||||||
|
|
||||||
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||||
self.size = try container.decode(Int64.self, forKey: .size)
|
self.size = try container.decode(Int64.self, forKey: .size)
|
||||||
|
|
||||||
|
self._minOSVersion = try container.decodeIfPresent(String.self, forKey: .minOSVersion)
|
||||||
|
self._maxOSVersion = try container.decodeIfPresent(String.self, forKey: .maxOSVersion)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -113,4 +118,13 @@ public extension AppVersion
|
|||||||
|
|
||||||
return appVersion
|
return appVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isSupported: Bool {
|
||||||
|
if let minOSVersion = self.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
|
||||||
|
return false
|
||||||
|
} else if let maxOSVersion = self.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import Roxas
|
|||||||
|
|
||||||
extension CFNotificationName
|
extension CFNotificationName
|
||||||
{
|
{
|
||||||
fileprivate static let willAccessDatabase = CFNotificationName("com.rileytestut.AltStore.WillAccessDatabase" as CFString)
|
fileprivate static let willMigrateDatabase = CFNotificationName("com.rileytestut.AltStore.WillMigrateDatabase" as CFString)
|
||||||
}
|
}
|
||||||
|
|
||||||
private let ReceivedWillAccessDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
|
private let ReceivedWillMigrateDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
|
||||||
DatabaseManager.shared.receivedWillAccessDatabaseNotification()
|
DatabaseManager.shared.receivedWillMigrateDatabaseNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate class PersistentContainer: RSTPersistentContainer
|
fileprivate class PersistentContainer: RSTPersistentContainer
|
||||||
@@ -52,15 +52,15 @@ public class DatabaseManager
|
|||||||
private let coordinator = NSFileCoordinator()
|
private let coordinator = NSFileCoordinator()
|
||||||
private let coordinatorQueue = OperationQueue()
|
private let coordinatorQueue = OperationQueue()
|
||||||
|
|
||||||
private var ignoreWillAccessDatabaseNotification = false
|
private var ignoreWillMigrateDatabaseNotification = false
|
||||||
|
|
||||||
private init()
|
private init()
|
||||||
{
|
{
|
||||||
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
|
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
|
||||||
self.persistentContainer.preferredMergePolicy = MergePolicy()
|
self.persistentContainer.preferredMergePolicy = MergePolicy()
|
||||||
|
|
||||||
let observer = Unmanaged.passUnretained(self).toOpaque()
|
let observer = Unmanaged.passUnretained(self).toOpaque()
|
||||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillAccessDatabaseNotification, CFNotificationName.willAccessDatabase.rawValue, nil, .deliverImmediately)
|
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillMigrateDatabaseNotification, CFNotificationName.willMigrateDatabase.rawValue, nil, .deliverImmediately)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,10 +87,13 @@ public extension DatabaseManager
|
|||||||
|
|
||||||
guard !self.isStarted else { return finish(nil) }
|
guard !self.isStarted else { return finish(nil) }
|
||||||
|
|
||||||
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
|
if self.persistentContainer.isMigrationRequired {
|
||||||
self.ignoreWillAccessDatabaseNotification = true
|
|
||||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willAccessDatabase, nil, nil, true)
|
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
|
||||||
|
self.ignoreWillMigrateDatabaseNotification = true
|
||||||
|
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willMigrateDatabase, nil, nil, true)
|
||||||
|
}
|
||||||
|
|
||||||
self.migrateDatabaseToAppGroupIfNeeded { (result) in
|
self.migrateDatabaseToAppGroupIfNeeded { (result) in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
@@ -229,7 +232,7 @@ private extension DatabaseManager
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
storeApp = StoreApp.makeAltStoreApp(in: context)
|
storeApp = StoreApp.makeAltStoreApp(in: context)
|
||||||
storeApp.latestVersion?.version = localApp.version
|
storeApp.latestSupportedVersion?.version = localApp.version
|
||||||
storeApp.source = altStoreSource
|
storeApp.source = altStoreSource
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,13 +420,13 @@ private extension DatabaseManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func receivedWillAccessDatabaseNotification()
|
func receivedWillMigrateDatabaseNotification()
|
||||||
{
|
{
|
||||||
defer { self.ignoreWillAccessDatabaseNotification = false }
|
defer { self.ignoreWillMigrateDatabaseNotification = false }
|
||||||
|
|
||||||
// Ignore notifications sent by the current process.
|
// Ignore notifications sent by the current process.
|
||||||
guard !self.ignoreWillAccessDatabaseNotification else { return }
|
guard !self.ignoreWillMigrateDatabaseNotification else { return }
|
||||||
|
|
||||||
exit(104)
|
exit(104)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
|
|||||||
|
|
||||||
@objc public var hasUpdate: Bool {
|
@objc public var hasUpdate: Bool {
|
||||||
if self.storeApp == nil { return false }
|
if self.storeApp == nil { return false }
|
||||||
if self.storeApp!.latestVersion == nil { return false }
|
if self.storeApp!.latestSupportedVersion == nil { return false }
|
||||||
|
|
||||||
let currentVersion = SemanticVersion(self.version)
|
let currentVersion = SemanticVersion(self.version)
|
||||||
let latestVersion = SemanticVersion(self.storeApp!.latestVersion!.version)
|
let latestVersion = SemanticVersion(self.storeApp!.latestSupportedVersion!.version)
|
||||||
|
|
||||||
if currentVersion == nil || latestVersion == nil {
|
if currentVersion == nil || latestVersion == nil {
|
||||||
// One of the versions is not valid SemVer, fall back to comparing the version strings by character
|
// One of the versions is not valid SemVer, fall back to comparing the version strings by character
|
||||||
return self.version < self.storeApp!.latestVersion!.version
|
return self.version < self.storeApp!.latestSupportedVersion!.version
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentVersion! < latestVersion!
|
return currentVersion! < latestVersion!
|
||||||
@@ -163,8 +163,8 @@ public extension InstalledApp
|
|||||||
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
|
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
|
||||||
{
|
{
|
||||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||||
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K == YES",
|
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
|
||||||
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.hasUpdate))
|
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestSupportedVersion.version))
|
||||||
return fetchRequest
|
return fetchRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ public extension InstalledApp
|
|||||||
|
|
||||||
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||||
{
|
{
|
||||||
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
let predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))")
|
print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))")
|
||||||
|
|
||||||
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||||
@@ -223,7 +223,7 @@ public extension InstalledApp
|
|||||||
// Date 6 hours before now.
|
// Date 6 hours before now.
|
||||||
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
||||||
|
|
||||||
var predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
|
let predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
|
||||||
#keyPath(InstalledApp.isActive),
|
#keyPath(InstalledApp.isActive),
|
||||||
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
||||||
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
@@ -275,14 +275,12 @@ public extension InstalledApp
|
|||||||
|
|
||||||
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
|
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
|
||||||
catch { print("Creating App Directory Error: \(error)") }
|
catch { print("Creating App Directory Error: \(error)") }
|
||||||
print("`appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
|
|
||||||
return appsDirectoryURL
|
return appsDirectoryURL
|
||||||
}
|
}
|
||||||
|
|
||||||
class var legacyAppsDirectoryURL: URL {
|
class var legacyAppsDirectoryURL: URL {
|
||||||
let baseDirectory = FileManager.default.applicationSupportDirectory
|
let baseDirectory = FileManager.default.applicationSupportDirectory
|
||||||
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
|
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
|
||||||
print("legacy `appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
|
|
||||||
return appsDirectoryURL
|
return appsDirectoryURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ extension LoggedError
|
|||||||
case deactivate
|
case deactivate
|
||||||
case backup
|
case backup
|
||||||
case restore
|
case restore
|
||||||
|
case connection
|
||||||
|
case enableJIT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +68,12 @@ public class LoggedError: NSManagedObject, Fetchable
|
|||||||
self.date = date
|
self.date = date
|
||||||
self._operation = operation?.rawValue
|
self._operation = operation?.rawValue
|
||||||
|
|
||||||
let nsError = error as NSError
|
let nsError: NSError
|
||||||
|
if let error = error as? ALTServerError, error.code == .underlyingError, let underlyingError = error.underlyingError {
|
||||||
|
nsError = underlyingError as NSError
|
||||||
|
} else {
|
||||||
|
nsError = error as NSError
|
||||||
|
}
|
||||||
self.domain = nsError.domain
|
self.domain = nsError.domain
|
||||||
self.code = Int32(nsError.code)
|
self.code = Int32(nsError.code)
|
||||||
self.userInfo = nsError.userInfo
|
self.userInfo = nsError.userInfo
|
||||||
@@ -91,7 +98,7 @@ public extension LoggedError
|
|||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
var error: Error {
|
var error: NSError {
|
||||||
let nsError = NSError(domain: self.domain, code: Int(self.code), userInfo: self.userInfo)
|
let nsError = NSError(domain: self.domain, code: Int(self.code), userInfo: self.userInfo)
|
||||||
return nsError
|
return nsError
|
||||||
}
|
}
|
||||||
@@ -113,6 +120,8 @@ public extension LoggedError
|
|||||||
case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), self.appName)
|
case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), self.appName)
|
||||||
case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), self.appName)
|
case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), self.appName)
|
||||||
case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), self.appName)
|
case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), self.appName)
|
||||||
|
case .connection: return String(format: NSLocalizedString("Connection during %@ Failed", comment: ""), self.appName)
|
||||||
|
case .enableJIT: return String(format: NSLocalizedString("Enabling JIT for %@ Failed", comment: ""), self.appName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
|
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
|
||||||
|
|
||||||
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
|
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
|
||||||
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestVersionApp?.latestVersion == $0 }),
|
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestSupportedVersionApp?.latestSupportedVersion == $0 }),
|
||||||
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
|
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
|
||||||
{
|
{
|
||||||
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
|
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ fileprivate extension NSManagedObject
|
|||||||
|
|
||||||
func setStoreAppLatestVersion(_ appVersion: NSManagedObject)
|
func setStoreAppLatestVersion(_ appVersion: NSManagedObject)
|
||||||
{
|
{
|
||||||
self.setValue(appVersion, forKey: #keyPath(StoreApp.latestVersion))
|
self.setValue(appVersion, forKey: #keyPath(StoreApp.latestSupportedVersion))
|
||||||
|
|
||||||
let versions = NSOrderedSet(array: [appVersion])
|
let versions = NSOrderedSet(array: [appVersion])
|
||||||
self.setValue(versions, forKey: #keyPath(StoreApp._versions))
|
self.setValue(versions, forKey: #keyPath(StoreApp._versions))
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
@NSManaged @objc(source) public var _source: Source?
|
@NSManaged @objc(source) public var _source: Source?
|
||||||
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet
|
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet
|
||||||
|
|
||||||
@NSManaged public private(set) var latestVersion: AppVersion?
|
@NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
|
||||||
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
|
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
|
||||||
|
|
||||||
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
|
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
|
||||||
@@ -169,31 +169,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
return self._versions.array as! [AppVersion]
|
return self._versions.array as! [AppVersion]
|
||||||
}
|
}
|
||||||
|
|
||||||
@nonobjc public var size: Int64? {
|
|
||||||
guard let version = self.latestVersion else { return nil }
|
|
||||||
return version.size
|
|
||||||
}
|
|
||||||
|
|
||||||
@nonobjc public var version: String? {
|
|
||||||
guard let version = self.latestVersion else { return nil }
|
|
||||||
return version.version
|
|
||||||
}
|
|
||||||
|
|
||||||
@nonobjc public var versionDescription: String? {
|
|
||||||
guard let version = self.latestVersion else { return nil }
|
|
||||||
return version.localizedDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
@nonobjc public var versionDate: Date? {
|
|
||||||
guard let version = self.latestVersion else { return nil }
|
|
||||||
return version.date
|
|
||||||
}
|
|
||||||
|
|
||||||
@nonobjc public var downloadURL: URL? {
|
|
||||||
guard let version = self.latestVersion else { return nil }
|
|
||||||
return version.downloadURL
|
|
||||||
}
|
|
||||||
|
|
||||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
{
|
{
|
||||||
super.init(entity: entity, insertInto: context)
|
super.init(entity: entity, insertInto: context)
|
||||||
@@ -314,16 +289,30 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension StoreApp
|
internal extension StoreApp
|
||||||
{
|
{
|
||||||
func setVersions(_ versions: [AppVersion])
|
func setVersions(_ versions: [AppVersion])
|
||||||
{
|
{
|
||||||
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
|
|
||||||
|
|
||||||
self.latestVersion = latestVersion
|
|
||||||
self._versions = NSOrderedSet(array: versions)
|
self._versions = NSOrderedSet(array: versions)
|
||||||
|
|
||||||
|
let latestSupportedVersion = versions.first(where: { $0.isSupported })
|
||||||
|
self.latestSupportedVersion = latestSupportedVersion
|
||||||
|
|
||||||
|
for case let version as AppVersion in self._versions
|
||||||
|
{
|
||||||
|
if version == latestSupportedVersion
|
||||||
|
{
|
||||||
|
version.latestSupportedVersionApp = self
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Ensure we replace any previous relationship when merging.
|
||||||
|
version.latestSupportedVersionApp = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve backwards compatibility by assigning legacy property values.
|
// Preserve backwards compatibility by assigning legacy property values.
|
||||||
|
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
|
||||||
self._version = latestVersion.version
|
self._version = latestVersion.version
|
||||||
self._versionDate = latestVersion.date
|
self._versionDate = latestVersion.date
|
||||||
self._versionDescription = latestVersion.localizedDescription
|
self._versionDescription = latestVersion.localizedDescription
|
||||||
@@ -334,6 +323,10 @@ private extension StoreApp
|
|||||||
|
|
||||||
public extension StoreApp
|
public extension StoreApp
|
||||||
{
|
{
|
||||||
|
var latestAvailableVersion: AppVersion? {
|
||||||
|
return self._versions.firstObject as? AppVersion
|
||||||
|
}
|
||||||
|
|
||||||
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
|
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
|
||||||
{
|
{
|
||||||
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
|
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ extension ALTApplication: AppProtocol
|
|||||||
extension StoreApp: AppProtocol
|
extension StoreApp: AppProtocol
|
||||||
{
|
{
|
||||||
public var url: URL? {
|
public var url: URL? {
|
||||||
return self.downloadURL
|
return self.latestAvailableVersion?.downloadURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,3 +50,17 @@ extension InstalledApp: AppProtocol
|
|||||||
return self.fileURL
|
return self.fileURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AppVersion: AppProtocol {
|
||||||
|
public var name: String {
|
||||||
|
return self.app?.name ?? self.bundleIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
public var bundleIdentifier: String {
|
||||||
|
return self.appBundleID
|
||||||
|
}
|
||||||
|
|
||||||
|
public var url: URL? {
|
||||||
|
return self.downloadURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
AltWidget/Assets.xcassets/AltStore.imageset/1024.png
vendored
Normal file
|
After Width: | Height: | Size: 846 KiB |
@@ -5,12 +5,12 @@
|
|||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Group 23_120.png",
|
"filename" : "1024.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Group 23_180.png",
|
"filename" : "1024.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "group16Copy2.pdf",
|
"filename" : "sidestore-logo.svg",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
17
AltWidget/Assets.xcassets/Badge.imageset/sidestore-logo.svg
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<title>sidestore-logo-svg</title>
|
||||||
|
<style>
|
||||||
|
.s0 { fill: none }
|
||||||
|
.s1 { fill: #bebbb4 }
|
||||||
|
</style>
|
||||||
|
<g id="Layer">
|
||||||
|
<g id="Layer">
|
||||||
|
<path id="Main" class="s0" />
|
||||||
|
<g id="Main1">
|
||||||
|
<g id="Layer">
|
||||||
|
<path id="Layer" fill-rule="evenodd" class="s1" d="m86 50c0 2.3-1.9 4.2-4.2 4.2h-50.9c-2.3 0-4.2-1.9-4.2-4.2 0-2.3 1.9-4.2 4.2-4.2h50.9c2.3 0 4.2 1.9 4.2 4.2zm-31.8-23.3v2.1c0 3.4-1.3 6.6-3.7 9-2.4 2.4-5.6 3.7-9 3.7h-10.6c-2.2 0-4.4 0.9-5.9 2.5-1.6 1.6-2.5 3.8-2.5 6 0 2.2 0.9 4.4 2.5 6 1.5 1.6 3.7 2.5 5.9 2.5h10.6c3.4 0 6.6 1.3 9 3.7 2.4 2.4 3.7 5.6 3.7 9v10.8c0 1.1-0.4 2.1-1.1 2.8-0.8 0.8-1.8 1.2-2.9 1.2h-0.4c-1.1 0-2.1-0.4-2.9-1.2-0.7-0.7-1.1-1.7-1.1-2.8v-10.8c0-1.1-0.5-2.2-1.3-3-0.8-0.8-1.8-1.3-3-1.3h-10.6c-4.5 0-8.8-1.7-11.9-4.9-3.2-3.2-5-7.5-5-12 0-4.5 1.8-8.8 5-12 3.1-3.2 7.4-4.9 11.9-4.9h10.6c1.2 0 2.2-0.5 3-1.3 0.8-0.8 1.3-1.9 1.3-3v-2.1h-4.3l8.5-12.7 8.5 12.7zm12.7 55.3c0 2.2-1.8 4-4 4h-0.5c-2.2 0-4-1.8-4-4v-19.5c0-2.2 1.8-4 4-4h0.5c2.2 0 4 1.8 4 4z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |