mirror of
https://github.com/SideStore/SideStore.git
synced 2026-03-31 07:45:40 +02:00
Compare commits
256 Commits
feature/Ro
...
users/june
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62faddf954 | ||
|
|
6a1181b21f | ||
|
|
c25ae10873 | ||
|
|
2842c8f669 | ||
|
|
3161892585 | ||
|
|
489843f987 | ||
|
|
dc0b30ab67 | ||
|
|
c3235cc554 | ||
|
|
6568e5918a | ||
|
|
91fba6db99 | ||
|
|
6b7e9a66f1 | ||
|
|
3682b65a4a | ||
|
|
117412645b | ||
|
|
c784ff6925 | ||
|
|
cf477024fc | ||
|
|
d595b7037f | ||
|
|
0c5007c8d8 | ||
|
|
8a87445d1f | ||
|
|
76a693fae4 | ||
|
|
9c150d5f4a | ||
|
|
e5febcdc6c | ||
|
|
1e969a0888 | ||
|
|
72bb549ea3 | ||
|
|
3c7cfdd91f | ||
|
|
b21f80cdd7 | ||
|
|
867a9c77e6 | ||
|
|
bc2d2c18fc | ||
|
|
ab923d245d | ||
|
|
fcf1b9ae03 | ||
|
|
59896e4f89 | ||
|
|
2a9f88c810 | ||
|
|
e98b0a3758 | ||
|
|
0cb6da7be4 | ||
|
|
fc3ff41fc4 | ||
|
|
719ddc8263 | ||
|
|
9f6b1284bb | ||
|
|
bfb4a90fdd | ||
|
|
d1b6bedc30 | ||
|
|
b78c75d829 | ||
|
|
4518f07b5b | ||
|
|
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 | ||
|
|
337d26333e | ||
|
|
ebb64d255b | ||
|
|
7dcb199f68 | ||
|
|
4334e887de |
21
.codecov.yml
21
.codecov.yml
@@ -1,21 +0,0 @@
|
|||||||
# https://docs.codecov.io/docs/codecov-yaml
|
|
||||||
|
|
||||||
codecov:
|
|
||||||
require_ci_to_pass: true
|
|
||||||
|
|
||||||
coverage:
|
|
||||||
precision: 2
|
|
||||||
round: down
|
|
||||||
range: "70...100"
|
|
||||||
ignore:
|
|
||||||
- Dependencies
|
|
||||||
status:
|
|
||||||
patch:
|
|
||||||
default:
|
|
||||||
if_no_uploads: error
|
|
||||||
changes: true
|
|
||||||
project:
|
|
||||||
default:
|
|
||||||
target: auto
|
|
||||||
if_no_uploads: error
|
|
||||||
comment: false
|
|
||||||
@@ -1,35 +1,39 @@
|
|||||||
# http://editorconfig.org
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
[*]
|
[*]
|
||||||
indent_style = space
|
|
||||||
charset = utf-8
|
|
||||||
indent_size = 4
|
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.{md,markdown}]
|
# Matches multiple files with brace expansion notation
|
||||||
trim_trailing_whitespace = false
|
# Set default charset
|
||||||
|
[*.{js,py}]
|
||||||
|
charset = utf-8# 4 space indentation
|
||||||
|
|
||||||
[*.{c,h,m,mm}]
|
# Swift files
|
||||||
trim_trailing_whitespace = true
|
[*.swift]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
charset = utf-8# 4 space indentation
|
||||||
|
|
||||||
|
# 4 space indentation
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Tab indentation (no size specified)
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
# Indentation override for all JS under lib directory
|
||||||
|
[lib/**.js]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.js]
|
# Matches the exact files either package.json or .travis.yml
|
||||||
indent_size = 2
|
[{package.json,.travis.yml}]
|
||||||
|
indent_style = space
|
||||||
[*.{swift}]
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
indent_style = tab
|
|
||||||
indent_size = 4
|
|
||||||
|
|
||||||
[Makefile]
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
indent_style = tab
|
|
||||||
indent_size = 8
|
|
||||||
|
|
||||||
[*.{yaml|yml}]
|
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|||||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
|||||||
* @JoeMatt @lonkelle
|
* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -2,15 +2,15 @@ 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: |
|
||||||
|
## Please note that the issue tracker is not for support
|
||||||
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
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
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
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
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# .github/workflows/sidestore-project.yml
|
|
||||||
name: SideStore Project
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: tuist/tuist-action@0.13.0
|
|
||||||
with:
|
|
||||||
command: 'build'
|
|
||||||
arguments: ''
|
|
||||||
|
|
||||||
55
.github/workflows/attach_build_products.yml
vendored
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);
|
||||||
|
}
|
||||||
|
|||||||
59
.github/workflows/beta.yml
vendored
59
.github/workflows/beta.yml
vendored
@@ -11,13 +11,13 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-12'
|
- os: 'macos-14'
|
||||||
version: '14.2'
|
version: '15.4'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -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
|
||||||
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,28 +55,12 @@ 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
|
||||||
|
|
||||||
- name: Get current date in SideStore date form
|
- name: Get current date in AltStore date form
|
||||||
id: date_sidestore
|
id: date_altstore
|
||||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload to new beta release
|
- name: Upload to new beta release
|
||||||
@@ -85,6 +83,21 @@ jobs:
|
|||||||
## Build Info
|
## Build Info
|
||||||
|
|
||||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
Built at (UTC date): `${{ steps.date_sidestore.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@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
61
.github/workflows/nightly.yml
vendored
61
.github/workflows/nightly.yml
vendored
@@ -14,13 +14,13 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-12'
|
- os: 'macos-14'
|
||||||
version: '14.2'
|
version: '15.4'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
run: brew install ldid
|
run: brew install ldid
|
||||||
|
|
||||||
- name: Cache .nightly-build-num
|
- name: Cache .nightly-build-num
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: .nightly-build-num
|
path: .nightly-build-num
|
||||||
key: nightly-build-num
|
key: nightly-build-num
|
||||||
@@ -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.6.0
|
||||||
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,28 +63,12 @@ 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
|
||||||
|
|
||||||
- name: Get current date in SideStore date form
|
- name: Get current date in AltStore date form
|
||||||
id: date_sidestore
|
id: date_altstore
|
||||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload to nightly release
|
- name: Upload to nightly release
|
||||||
@@ -92,9 +89,21 @@ jobs:
|
|||||||
## Build Info
|
## Build Info
|
||||||
|
|
||||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
Built at (UTC date): `${{ steps.date_sidestore.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: 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@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./*.dSYM/
|
||||||
|
|||||||
38
.github/workflows/pr.yml
vendored
38
.github/workflows/pr.yml
vendored
@@ -9,13 +9,13 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-12'
|
- os: 'macos-14'
|
||||||
version: '14.2'
|
version: '15.4'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -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.6.0
|
||||||
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@v4
|
||||||
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@v4
|
||||||
with:
|
with:
|
||||||
name: SideStore-dSYM
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
path: ./*.dSYM/
|
path: ./*.dSYM/
|
||||||
|
|||||||
58
.github/workflows/stable.yml
vendored
58
.github/workflows/stable.yml
vendored
@@ -11,13 +11,13 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-12'
|
- os: 'macos-14'
|
||||||
version: '14.2'
|
version: '15.4'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -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.6.0
|
||||||
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,28 +54,12 @@ 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
|
||||||
|
|
||||||
- name: Get current date in SideStore date form
|
- name: Get current date in AltStore date form
|
||||||
id: date_sidestore
|
id: date_altstore
|
||||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload to new stable release
|
- name: Upload to new stable release
|
||||||
@@ -82,6 +79,21 @@ jobs:
|
|||||||
## Build Info
|
## Build Info
|
||||||
|
|
||||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
Built at (UTC date): `${{ steps.date_sidestore.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@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./*.dSYM/
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,7 +19,6 @@ archive.xcarchive
|
|||||||
*.perspectivev3
|
*.perspectivev3
|
||||||
!default.perspectivev3
|
!default.perspectivev3
|
||||||
xcuserdata
|
xcuserdata
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
*.xccheckout
|
*.xccheckout
|
||||||
*.moved-aside
|
*.moved-aside
|
||||||
|
|||||||
27
.gitmodules
vendored
27
.gitmodules
vendored
@@ -1,6 +1,21 @@
|
|||||||
[submodule "Dependencies/em_proxy"]
|
[submodule "Dependencies/Roxas"]
|
||||||
path = SideStoreApp/Dependencies/em_proxy
|
path = Dependencies/Roxas
|
||||||
url = https://github.com/SideStore/em_proxy.git
|
url = https://github.com/rileytestut/Roxas.git
|
||||||
[submodule "Dependencies/minimuxer"]
|
[submodule "Dependencies/libimobiledevice"]
|
||||||
path = SideStoreApp/Dependencies/minimuxer
|
path = Dependencies/libimobiledevice
|
||||||
url = https://github.com/SideStore/minimuxer.git
|
url = https://github.com/libimobiledevice/libimobiledevice
|
||||||
|
[submodule "Dependencies/libusbmuxd"]
|
||||||
|
path = Dependencies/libusbmuxd
|
||||||
|
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||||
|
[submodule "Dependencies/libplist"]
|
||||||
|
path = Dependencies/libplist
|
||||||
|
url = https://github.com/SideStore/libplist.git
|
||||||
|
[submodule "Dependencies/MarkdownAttributedString"]
|
||||||
|
path = Dependencies/MarkdownAttributedString
|
||||||
|
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||||
|
[submodule "Dependencies/libimobiledevice-glue"]
|
||||||
|
path = Dependencies/libimobiledevice-glue
|
||||||
|
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
||||||
|
[submodule "Dependencies/libfragmentzip"]
|
||||||
|
path = Dependencies/libfragmentzip
|
||||||
|
url = https://github.com/SideStore/libfragmentzip.git
|
||||||
|
|||||||
28
.jazzy.yaml
28
.jazzy.yaml
@@ -1,28 +0,0 @@
|
|||||||
# ---- About ----
|
|
||||||
module: SideStore
|
|
||||||
module_version: 1.0,0
|
|
||||||
author: SideStore
|
|
||||||
readme: README.md
|
|
||||||
copyright: 'See [license](https://github.com/SideStore/SideStore/blob/develop/LICENSE) for more details.'
|
|
||||||
|
|
||||||
# ---- URLs ----
|
|
||||||
author_url: https://sidestore.io
|
|
||||||
dash_url: https://sidestore.io/docsets/SideStore.xml
|
|
||||||
github_url: https://github.com/SideStore/SideStore/
|
|
||||||
github_file_prefix: https://github.com/SideStore/SideStore/tree/1.0.2/
|
|
||||||
|
|
||||||
# ---- Sources ----
|
|
||||||
source_directory: Sources
|
|
||||||
documentation: .build/x86_64-apple-macosx/debug/SideStore.docc
|
|
||||||
|
|
||||||
# ---- Generation ----
|
|
||||||
clean: true
|
|
||||||
output: docs
|
|
||||||
min_acl: public
|
|
||||||
hide_documentation_coverage: false
|
|
||||||
skip_undocumented: false
|
|
||||||
objc: false
|
|
||||||
swift_version: 5.1.0
|
|
||||||
|
|
||||||
# ---- Formatting ----
|
|
||||||
theme: fullwidth
|
|
||||||
42
.swiftformat
42
.swiftformat
@@ -1,42 +0,0 @@
|
|||||||
# .swiftformat
|
|
||||||
|
|
||||||
## file options
|
|
||||||
|
|
||||||
--exclude .build,.github,.swiftpm,.vscode,Configurations,Dependencies
|
|
||||||
|
|
||||||
## format options
|
|
||||||
|
|
||||||
--allman false
|
|
||||||
--binarygrouping 4,8
|
|
||||||
--commas always
|
|
||||||
--comments indent
|
|
||||||
--decimalgrouping 3,6
|
|
||||||
--elseposition same-line
|
|
||||||
--empty void
|
|
||||||
--exponentcase lowercase
|
|
||||||
--exponentgrouping disabled
|
|
||||||
--fractiongrouping disabled
|
|
||||||
--header ignore
|
|
||||||
--hexgrouping 4,8
|
|
||||||
--hexliteralcase uppercase
|
|
||||||
--ifdef indent
|
|
||||||
--importgrouping testable-bottom
|
|
||||||
--indent 4
|
|
||||||
--indentcase false
|
|
||||||
--linebreaks lf
|
|
||||||
--maxwidth none
|
|
||||||
--octalgrouping 4,8
|
|
||||||
--operatorfunc spaced
|
|
||||||
--patternlet hoist
|
|
||||||
--ranges spaced
|
|
||||||
--self remove
|
|
||||||
--semicolons inline
|
|
||||||
--stripunusedargs always
|
|
||||||
--swiftversion 5.1
|
|
||||||
--trimwhitespace always
|
|
||||||
--wraparguments preserve
|
|
||||||
--wrapcollections preserve
|
|
||||||
|
|
||||||
## rules
|
|
||||||
|
|
||||||
--enable isEmpty,andOperator,assertionFailures
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
disabled_rules:
|
|
||||||
- block_based_kvo
|
|
||||||
- colon
|
|
||||||
- control_statement
|
|
||||||
- cyclomatic_complexity
|
|
||||||
- discarded_notification_center_observer
|
|
||||||
- file_length
|
|
||||||
- function_parameter_count
|
|
||||||
- generic_type_name
|
|
||||||
- identifier_name
|
|
||||||
- multiple_closures_with_trailing_closure
|
|
||||||
- nesting
|
|
||||||
- switch_case_alignment
|
|
||||||
- todo
|
|
||||||
- type_name
|
|
||||||
- type_body_length
|
|
||||||
- function_body_length
|
|
||||||
- unused_closure_parameter
|
|
||||||
|
|
||||||
# parameterized rules can be customized from this configuration file
|
|
||||||
line_length: 200
|
|
||||||
# parameterized rules are first parameterized as a warning level, then error level.
|
|
||||||
type_body_length:
|
|
||||||
- 300 # warning
|
|
||||||
- 600 # error
|
|
||||||
# parameterized rules are first parameterized as a warning level, then error level.
|
|
||||||
# identifier_name_max_length:
|
|
||||||
# - 40 # warning
|
|
||||||
# - 60 # error
|
|
||||||
# # parameterized rules are first parameterized as a warning level, then error level.
|
|
||||||
# identifier_name_min_length:
|
|
||||||
# - 3 # warning
|
|
||||||
# - 2 # error
|
|
||||||
function_body_length:
|
|
||||||
- 200 # warning
|
|
||||||
- 500 # error
|
|
||||||
large_tuple:
|
|
||||||
- 4 # warning
|
|
||||||
- 6 # error
|
|
||||||
|
|
||||||
opt_in_rules:
|
|
||||||
- empty_count
|
|
||||||
- force_unwrapping
|
|
||||||
|
|
||||||
excluded: # paths to ignore during linting. overridden byincluded.
|
|
||||||
- .build
|
|
||||||
- .github
|
|
||||||
- .swiftpm
|
|
||||||
- .vscode
|
|
||||||
- Dependencies
|
|
||||||
|
|
||||||
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
|
|
||||||
- explicit_self
|
|
||||||
|
|
||||||
# Override these rules to be warnings for now
|
|
||||||
force_cast: warning
|
|
||||||
force_try: warning
|
|
||||||
empty_count: warning
|
|
||||||
|
|
||||||
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit)
|
|
||||||
|
|
||||||
custom_rules:
|
|
||||||
placeholders_in_comments:
|
|
||||||
included: ".*\\.swift"
|
|
||||||
name: "No Placeholders in Comments"
|
|
||||||
regex: "<#([^#]+)#>"
|
|
||||||
match_kinds:
|
|
||||||
- comment
|
|
||||||
- doccomment
|
|
||||||
message: "Placeholder left in comment."
|
|
||||||
tiles_deprecated:
|
|
||||||
included: ".*\\.swift"
|
|
||||||
name: "Tiles are deprecated in favor of Frame"
|
|
||||||
regex: "([T,t]ile$|^[T,t]il[e,es])"
|
|
||||||
message: "Tiles are deprecated in favor of Frame"
|
|
||||||
severity: warning
|
|
||||||
3
AltBackup.xcconfig
Normal file
3
AltBackup.xcconfig
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#include "Build.xcconfig"
|
||||||
|
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup
|
||||||
@@ -7,12 +7,9 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import OSLog
|
|
||||||
#if canImport(Logging)
|
|
||||||
import Logging
|
|
||||||
#endif
|
|
||||||
|
|
||||||
extension AppDelegate {
|
extension AppDelegate
|
||||||
|
{
|
||||||
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
|
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
|
||||||
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
|
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
|
||||||
|
|
||||||
@@ -23,58 +20,64 @@ extension AppDelegate {
|
|||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
private var currentBackupReturnURL: URL?
|
private var currentBackupReturnURL: URL?
|
||||||
|
|
||||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||||
|
{
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
|
||||||
|
|
||||||
let viewController = ViewController()
|
let viewController = ViewController()
|
||||||
|
|
||||||
window = UIWindow(frame: UIScreen.main.bounds)
|
self.window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
window?.rootViewController = viewController
|
self.window?.rootViewController = viewController
|
||||||
window?.makeKeyAndVisible()
|
self.window?.makeKeyAndVisible()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillResignActive(_: UIApplication) {
|
func applicationWillResignActive(_ application: UIApplication) {
|
||||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidBecomeActive(_: UIApplication) {
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_: UIApplication) {
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||||
open(url)
|
{
|
||||||
|
return self.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AppDelegate {
|
private extension AppDelegate
|
||||||
func open(_ url: URL) -> Bool {
|
{
|
||||||
|
func open(_ url: URL) -> Bool
|
||||||
|
{
|
||||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||||
guard let command = components.host?.lowercased() else { return false }
|
guard let command = components.host?.lowercased() else { return false }
|
||||||
|
|
||||||
switch command {
|
switch command
|
||||||
|
{
|
||||||
case "backup":
|
case "backup":
|
||||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||||
currentBackupReturnURL = returnURL
|
self.currentBackupReturnURL = returnURL
|
||||||
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
|
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case "restore":
|
case "restore":
|
||||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||||
currentBackupReturnURL = returnURL
|
self.currentBackupReturnURL = returnURL
|
||||||
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
|
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -83,21 +86,23 @@ private extension AppDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func operationDidFinish(_ notification: Notification) {
|
@objc func operationDidFinish(_ notification: Notification)
|
||||||
|
{
|
||||||
defer { self.currentBackupReturnURL = nil }
|
defer { self.currentBackupReturnURL = nil }
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let returnURL = currentBackupReturnURL,
|
let returnURL = self.currentBackupReturnURL,
|
||||||
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
|
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
|
||||||
|
|
||||||
switch result {
|
switch result
|
||||||
|
{
|
||||||
case .success:
|
case .success:
|
||||||
components.path = "/success"
|
components.path = "/success"
|
||||||
|
|
||||||
case let .failure(error as NSError):
|
case .failure(let error as NSError):
|
||||||
components.path = "/failure"
|
components.path = "/failure"
|
||||||
components.queryItems = ["errorDomain": error.domain,
|
components.queryItems = ["errorDomain": error.domain,
|
||||||
"errorCode": String(error.code),
|
"errorCode": String(error.code),
|
||||||
@@ -107,9 +112,10 @@ private extension AppDelegate {
|
|||||||
guard let responseURL = components.url else { return }
|
guard let responseURL = components.url else { return }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
UIApplication.shared.open(responseURL, options: [:]) { success in
|
UIApplication.shared.open(responseURL, options: [:]) { (success) in
|
||||||
os_log("Sent response to app with success: %@", type: .info , success)
|
print("Sent response to app with success:", success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,21 +7,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
|
||||||
#if canImport(Logging)
|
|
||||||
import Logging
|
|
||||||
#endif
|
|
||||||
|
|
||||||
import AltSign
|
extension ErrorUserInfoKey
|
||||||
import Roxas
|
{
|
||||||
import protocol SideStoreCore.ALTLocalizedError
|
|
||||||
|
|
||||||
extension ErrorUserInfoKey {
|
|
||||||
static let sourceFile: String = "alt_sourceFile"
|
static let sourceFile: String = "alt_sourceFile"
|
||||||
static let sourceFileLine: String = "alt_sourceFileLine"
|
static let sourceFileLine: String = "alt_sourceFileLine"
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Error {
|
extension Error
|
||||||
|
{
|
||||||
var sourceDescription: String? {
|
var sourceDescription: String? {
|
||||||
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
|
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
|
||||||
return nil
|
return nil
|
||||||
@@ -30,8 +24,10 @@ extension Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BackupError: ALTLocalizedError {
|
struct BackupError: ALTLocalizedError
|
||||||
enum Code {
|
{
|
||||||
|
enum Code
|
||||||
|
{
|
||||||
case invalidBundleID
|
case invalidBundleID
|
||||||
case appGroupNotFound(String?)
|
case appGroupNotFound(String?)
|
||||||
case randomError // Used for debugging.
|
case randomError // Used for debugging.
|
||||||
@@ -45,45 +41,54 @@ struct BackupError: ALTLocalizedError {
|
|||||||
var failure: String?
|
var failure: String?
|
||||||
|
|
||||||
var failureReason: String? {
|
var failureReason: String? {
|
||||||
switch code {
|
switch self.code
|
||||||
|
{
|
||||||
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
|
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
|
||||||
case let .appGroupNotFound(appGroup):
|
case .appGroupNotFound(let appGroup):
|
||||||
if let appGroup = appGroup {
|
if let appGroup = appGroup
|
||||||
|
{
|
||||||
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
|
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
|
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
|
||||||
}
|
}
|
||||||
case .randomError: return NSLocalizedString("A random error occured.", comment: "")
|
case .randomError: return NSLocalizedString("A random error occured.", comment: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorUserInfo: [String: Any] {
|
var errorUserInfo: [String : Any] {
|
||||||
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: errorDescription,
|
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription,
|
||||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
NSLocalizedFailureReasonErrorKey: self.failureReason,
|
||||||
NSLocalizedFailureErrorKey: failure,
|
NSLocalizedFailureErrorKey: self.failure,
|
||||||
ErrorUserInfoKey.sourceFile: sourceFile,
|
ErrorUserInfoKey.sourceFile: self.sourceFile,
|
||||||
ErrorUserInfoKey.sourceFileLine: sourceFileLine]
|
ErrorUserInfoKey.sourceFileLine: self.sourceFileLine]
|
||||||
return userInfo.compactMapValues { $0 }
|
return userInfo.compactMapValues { $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line) {
|
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
|
||||||
|
{
|
||||||
self.code = code
|
self.code = code
|
||||||
failure = description
|
self.failure = description
|
||||||
sourceFile = file
|
self.sourceFile = file
|
||||||
sourceFileLine = line
|
self.sourceFileLine = line
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackupController: NSObject {
|
class BackupController: NSObject
|
||||||
|
{
|
||||||
private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
|
private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
|
||||||
private let operationQueue = OperationQueue()
|
private let operationQueue = OperationQueue()
|
||||||
|
|
||||||
override init() {
|
override init()
|
||||||
operationQueue.name = "AltBackup-BackupQueue"
|
{
|
||||||
|
self.operationQueue.name = "AltBackup-BackupQueue"
|
||||||
}
|
}
|
||||||
|
|
||||||
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
do {
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
|
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
|
||||||
}
|
}
|
||||||
@@ -101,24 +106,29 @@ class BackupController: NSObject {
|
|||||||
|
|
||||||
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
|
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
|
||||||
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
|
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
|
||||||
fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: operationQueue) { error in
|
self.fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: self.operationQueue) { (error) in
|
||||||
do {
|
do
|
||||||
if let error = error {
|
{
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
|
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
|
||||||
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
|
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path) {
|
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path)
|
||||||
|
{
|
||||||
try FileManager.default.removeItem(at: backupDocumentsDirectory)
|
try FileManager.default.removeItem(at: backupDocumentsDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: documentsDirectory.path) {
|
if FileManager.default.fileExists(atPath: documentsDirectory.path)
|
||||||
|
{
|
||||||
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
|
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,18 +137,21 @@ class BackupController: NSObject {
|
|||||||
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
||||||
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path) {
|
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path)
|
||||||
|
{
|
||||||
try FileManager.default.removeItem(at: backupLibraryDirectory)
|
try FileManager.default.removeItem(at: backupLibraryDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: libraryDirectory.path) {
|
if FileManager.default.fileExists(atPath: libraryDirectory.path)
|
||||||
|
{
|
||||||
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
|
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
|
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
|
||||||
}
|
}
|
||||||
|
|
||||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup {
|
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
|
||||||
|
{
|
||||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
|
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
|
||||||
}
|
}
|
||||||
@@ -152,22 +165,29 @@ class BackupController: NSObject {
|
|||||||
// Replace previous backup with new backup.
|
// Replace previous backup with new backup.
|
||||||
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
|
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
|
||||||
|
|
||||||
os_log("Replaced previous backup with new backup: %@", type: .info, temporaryAppBackupDirectory.absoluteString)
|
print("Replaced previous backup with new backup:", temporaryAppBackupDirectory)
|
||||||
|
|
||||||
completionHandler(.success(()))
|
completionHandler(.success(()))
|
||||||
} catch {
|
}
|
||||||
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) } catch { os_log("Failed to remove temporary directory. %@", type: .error , error.localizedDescription) }
|
catch
|
||||||
|
{
|
||||||
|
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) }
|
||||||
|
catch { print("Failed to remove temporary directory.", error) }
|
||||||
|
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
do {
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
|
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
|
||||||
}
|
}
|
||||||
@@ -181,9 +201,11 @@ class BackupController: NSObject {
|
|||||||
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
||||||
|
|
||||||
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
|
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
|
||||||
fileCoordinator.coordinate(with: [readingIntent], queue: operationQueue) { error in
|
self.fileCoordinator.coordinate(with: [readingIntent], queue: self.operationQueue) { (error) in
|
||||||
do {
|
do
|
||||||
if let error = error {
|
{
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +220,8 @@ class BackupController: NSObject {
|
|||||||
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
|
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
|
||||||
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
|
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
|
||||||
|
|
||||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup {
|
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
|
||||||
|
{
|
||||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
|
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
|
||||||
}
|
}
|
||||||
@@ -208,35 +231,46 @@ class BackupController: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
completionHandler(.success(()))
|
completionHandler(.success(()))
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension BackupController {
|
private extension BackupController
|
||||||
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws {
|
{
|
||||||
|
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws
|
||||||
|
{
|
||||||
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
|
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
|
||||||
|
|
||||||
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path) {
|
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path)
|
||||||
|
{
|
||||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options) {
|
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options)
|
||||||
|
{
|
||||||
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
|
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
|
||||||
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
|
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
if FileManager.default.fileExists(atPath: destinationURL.path)
|
||||||
|
{
|
||||||
do {
|
do {
|
||||||
try FileManager.default.removeItem(at: destinationURL)
|
try FileManager.default.removeItem(at: destinationURL)
|
||||||
} catch CocoaError.fileWriteNoPermission where isDirectory {
|
}
|
||||||
try copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
|
catch CocoaError.fileWriteNoPermission where isDirectory {
|
||||||
|
try self.copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
|
||||||
continue
|
continue
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
print(error)
|
print(error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -245,10 +279,12 @@ private extension BackupController {
|
|||||||
do {
|
do {
|
||||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||||
print("Copied item from \(fileURL) to \(destinationURL)")
|
print("Copied item from \(fileURL) to \(destinationURL)")
|
||||||
} catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
|
}
|
||||||
|
catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
|
||||||
// Ignore errors for /Documents/Inbox
|
// Ignore errors for /Documents/Inbox
|
||||||
os_log("Failed to copy Inbox directory: %@", type: .error , error.localizedDescription)
|
print("Failed to copy Inbox directory:", error)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
print(error)
|
print(error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -35,16 +35,6 @@
|
|||||||
<string>altbackup</string>
|
<string>altbackup</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Editor</string>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string>SideBackup General</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>sidebackup</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension UIColor {
|
extension UIColor
|
||||||
|
{
|
||||||
static let altstoreBackground = UIColor(named: "Background")!
|
static let altstoreBackground = UIColor(named: "Background")!
|
||||||
static let altstoreText = UIColor(named: "Text")!
|
static let altstoreText = UIColor(named: "Text")!
|
||||||
}
|
}
|
||||||
206
AltBackup/ViewController.swift
Normal file
206
AltBackup/ViewController.swift
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
//
|
||||||
|
// ViewController.swift
|
||||||
|
// AltBackup
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/11/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension Bundle
|
||||||
|
{
|
||||||
|
var appName: String? {
|
||||||
|
let appName =
|
||||||
|
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ??
|
||||||
|
Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
|
||||||
|
return appName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ViewController
|
||||||
|
{
|
||||||
|
enum BackupOperation
|
||||||
|
{
|
||||||
|
case backup
|
||||||
|
case restore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewController: UIViewController
|
||||||
|
{
|
||||||
|
private let backupController = BackupController()
|
||||||
|
|
||||||
|
private var currentOperation: BackupOperation? {
|
||||||
|
didSet {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var textLabel: UILabel!
|
||||||
|
private var detailTextLabel: UILabel!
|
||||||
|
private var activityIndicatorView: UIActivityIndicatorView!
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .lightContent
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
||||||
|
{
|
||||||
|
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.view.backgroundColor = .altstoreBackground
|
||||||
|
|
||||||
|
self.textLabel = UILabel(frame: .zero)
|
||||||
|
self.textLabel.font = UIFont.preferredFont(forTextStyle: .title2)
|
||||||
|
self.textLabel.textColor = .altstoreText
|
||||||
|
self.textLabel.textAlignment = .center
|
||||||
|
self.textLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
self.detailTextLabel = UILabel(frame: .zero)
|
||||||
|
self.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
self.detailTextLabel.textColor = .altstoreText
|
||||||
|
self.detailTextLabel.textAlignment = .center
|
||||||
|
self.detailTextLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
self.activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
|
||||||
|
self.activityIndicatorView.color = .altstoreText
|
||||||
|
self.activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
let button1 = UIButton(type: .system)
|
||||||
|
button1.setTitle("Backup", for: .normal)
|
||||||
|
button1.setTitleColor(.white, for: .normal)
|
||||||
|
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||||
|
|
||||||
|
let button2 = UIButton(type: .system)
|
||||||
|
button2.setTitle("Restore", for: .normal)
|
||||||
|
button2.setTitleColor(.white, for: .normal)
|
||||||
|
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||||
|
|
||||||
|
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
||||||
|
#else
|
||||||
|
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
|
||||||
|
#endif
|
||||||
|
|
||||||
|
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.spacing = 22
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.alignment = .center
|
||||||
|
self.view.addSubview(stackView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||||
|
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0),
|
||||||
|
self.view.safeAreaLayoutGuide.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)])
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ViewController
|
||||||
|
{
|
||||||
|
@objc func backup()
|
||||||
|
{
|
||||||
|
self.currentOperation = .backup
|
||||||
|
|
||||||
|
self.backupController.performBackup { (result) in
|
||||||
|
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||||
|
|
||||||
|
let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName)
|
||||||
|
self.process(result, errorTitle: title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func restore()
|
||||||
|
{
|
||||||
|
self.currentOperation = .restore
|
||||||
|
|
||||||
|
self.backupController.restoreBackup { (result) in
|
||||||
|
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||||
|
|
||||||
|
let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName)
|
||||||
|
self.process(result, errorTitle: title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
switch self.currentOperation
|
||||||
|
{
|
||||||
|
case .backup:
|
||||||
|
self.textLabel.text = NSLocalizedString("Backing up app data…", comment: "")
|
||||||
|
self.detailTextLabel.isHidden = true
|
||||||
|
self.activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
|
case .restore:
|
||||||
|
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
|
||||||
|
self.detailTextLabel.isHidden = true
|
||||||
|
self.activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
|
case .none:
|
||||||
|
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
|
||||||
|
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
|
||||||
|
|
||||||
|
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in SideStore to continue using it.", comment: ""),
|
||||||
|
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
|
||||||
|
|
||||||
|
self.detailTextLabel.isHidden = false
|
||||||
|
self.activityIndicatorView.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ViewController
|
||||||
|
{
|
||||||
|
func process(_ result: Result<Void, Error>, errorTitle: String)
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success: break
|
||||||
|
case .failure(let error as NSError):
|
||||||
|
let message: String
|
||||||
|
|
||||||
|
if let sourceDescription = error.sourceDescription
|
||||||
|
{
|
||||||
|
message = error.localizedDescription + "\n\n" + sourceDescription
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message = error.localizedDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert)
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||||
|
self.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didEnterBackground(_ notification: Notification)
|
||||||
|
{
|
||||||
|
// Reset UI once we've left app (but not before).
|
||||||
|
self.currentOperation = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,23 +10,27 @@ import Foundation
|
|||||||
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
private extension UserDefaults {
|
private extension UserDefaults
|
||||||
|
{
|
||||||
@objc var localUserID: String? {
|
@objc var localUserID: String? {
|
||||||
get { string(forKey: #keyPath(UserDefaults.localUserID)) }
|
get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
|
||||||
set { set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AnisetteDataManager {
|
struct AnisetteDataManager
|
||||||
|
{
|
||||||
static let shared = AnisetteDataManager()
|
static let shared = AnisetteDataManager()
|
||||||
|
|
||||||
private let dateFormatter = ISO8601DateFormatter()
|
private let dateFormatter = ISO8601DateFormatter()
|
||||||
|
|
||||||
private init() {
|
private init()
|
||||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW)
|
{
|
||||||
|
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestAnisetteData() throws -> ALTAnisetteData {
|
func requestAnisetteData() throws -> ALTAnisetteData
|
||||||
|
{
|
||||||
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
|
|
||||||
@@ -37,10 +41,11 @@ struct AnisetteDataManager {
|
|||||||
let headers = session.appleIDHeaders(for: request)
|
let headers = session.appleIDHeaders(for: request)
|
||||||
|
|
||||||
let device = akDevice.current
|
let device = akDevice.current
|
||||||
let date = dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
||||||
|
|
||||||
var localUserID = UserDefaults.standard.localUserID
|
var localUserID = UserDefaults.standard.localUserID
|
||||||
if localUserID == nil {
|
if localUserID == nil
|
||||||
|
{
|
||||||
localUserID = UUID().uuidString
|
localUserID = UUID().uuidString
|
||||||
UserDefaults.standard.localUserID = localUserID
|
UserDefaults.standard.localUserID = localUserID
|
||||||
}
|
}
|
||||||
@@ -10,15 +10,18 @@ import Foundation
|
|||||||
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
private extension URL {
|
private extension URL
|
||||||
|
{
|
||||||
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
|
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CFNotificationName {
|
private extension CFNotificationName
|
||||||
|
{
|
||||||
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
|
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppManager {
|
struct AppManager
|
||||||
|
{
|
||||||
static let shared = AppManager()
|
static let shared = AppManager()
|
||||||
|
|
||||||
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
|
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
|
||||||
@@ -26,24 +29,27 @@ struct AppManager {
|
|||||||
|
|
||||||
private let fileCoordinator = NSFileCoordinator()
|
private let fileCoordinator = NSFileCoordinator()
|
||||||
|
|
||||||
private init() {
|
private init()
|
||||||
profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
{
|
||||||
profilesQueue.qualityOfService = .userInitiated
|
self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
||||||
|
self.profilesQueue.qualityOfService = .userInitiated
|
||||||
}
|
}
|
||||||
|
|
||||||
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles _: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
appQueue.async {
|
{
|
||||||
|
self.appQueue.async {
|
||||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||||
|
|
||||||
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String: Any]
|
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
|
||||||
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
|
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
|
||||||
|
|
||||||
completionHandler(result)
|
completionHandler(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
appQueue.async {
|
{
|
||||||
|
self.appQueue.async {
|
||||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||||
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
|
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
|
||||||
|
|
||||||
@@ -51,11 +57,14 @@ struct AppManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||||
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in
|
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||||
do {
|
do
|
||||||
if let error = error {
|
{
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,24 +73,31 @@ struct AppManager {
|
|||||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||||
|
|
||||||
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
|
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
|
||||||
for fileURL in profileURLs {
|
for fileURL in profileURLs
|
||||||
|
{
|
||||||
// Use memory mapping to reduce peak memory usage and stay within limit.
|
// Use memory mapping to reduce peak memory usage and stay within limit.
|
||||||
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
|
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
|
||||||
|
|
||||||
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile) {
|
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
|
||||||
|
{
|
||||||
try FileManager.default.removeItem(at: fileURL)
|
try FileManager.default.removeItem(at: fileURL)
|
||||||
} else {
|
}
|
||||||
os_log("Ignoring: %@ %@", type: .info , profile.bundleIdentifier, profile.uuid)
|
else
|
||||||
|
{
|
||||||
|
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for profile in profiles {
|
for profile in profiles
|
||||||
|
{
|
||||||
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
|
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
|
||||||
try profile.data.write(to: destinationURL, options: .atomic)
|
try profile.data.write(to: destinationURL, options: .atomic)
|
||||||
}
|
}
|
||||||
|
|
||||||
completionHandler(.success(()))
|
completionHandler(.success(()))
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,22 +106,28 @@ struct AppManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||||
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in
|
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||||
|
|
||||||
for fileURL in profileURLs {
|
for fileURL in profileURLs
|
||||||
|
{
|
||||||
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
||||||
|
|
||||||
if bundleIdentifiers.contains(profile.bundleIdentifier) {
|
if bundleIdentifiers.contains(profile.bundleIdentifier)
|
||||||
|
{
|
||||||
try FileManager.default.removeItem(at: fileURL)
|
try FileManager.default.removeItem(at: fileURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
completionHandler(.success(()))
|
completionHandler(.success(()))
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,69 +8,75 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import SideKit
|
|
||||||
import OSLog
|
|
||||||
#if canImport(Logging)
|
|
||||||
import Logging
|
|
||||||
#endif
|
|
||||||
|
|
||||||
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
|
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
|
||||||
|
|
||||||
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
|
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
|
||||||
connectionHandlers: [XPCConnectionHandler()])
|
connectionHandlers: [XPCConnectionHandler()])
|
||||||
|
|
||||||
extension DaemonConnectionManager {
|
extension DaemonConnectionManager
|
||||||
|
{
|
||||||
static var shared: ConnectionManager {
|
static var shared: ConnectionManager {
|
||||||
connectionManager
|
return connectionManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DaemonRequestHandler: RequestHandler {
|
struct DaemonRequestHandler: RequestHandler
|
||||||
func handleAnisetteDataRequest(_: AnisetteDataRequest, for _: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void) {
|
{
|
||||||
do {
|
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
|
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
|
||||||
|
|
||||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||||
completionHandler(.success(response))
|
completionHandler(.success(response))
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void) {
|
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
||||||
|
{
|
||||||
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
|
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
|
||||||
|
|
||||||
print("Awaiting begin installation request...")
|
print("Awaiting begin installation request...")
|
||||||
|
|
||||||
connection.receiveRequest { result in
|
connection.receiveRequest() { (result) in
|
||||||
os_log("Received begin installation request with result: %@", type: .info , String(describing: result))
|
print("Received begin installation request with result:", result)
|
||||||
|
|
||||||
do {
|
do
|
||||||
guard case let .beginInstallation(request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
{
|
||||||
|
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
||||||
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
|
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
|
||||||
|
|
||||||
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { result in
|
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
|
||||||
let result = result.map { InstallationProgressResponse(progress: 1.0) }
|
let result = result.map { InstallationProgressResponse(progress: 1.0) }
|
||||||
os_log("Installed app with result: %@", type: .info, String(describing: result))
|
print("Installed app with result:", result)
|
||||||
|
|
||||||
completionHandler(result)
|
completionHandler(result)
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for _: Connection,
|
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
||||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void) {
|
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
||||||
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { result in
|
{
|
||||||
switch result {
|
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
|
||||||
case let .failure(error):
|
switch result
|
||||||
os_log("Failed to install profiles %@ : %@", type: .error , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n"), error.localizedDescription)
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
|
|
||||||
case .success:
|
case .success:
|
||||||
os_log("Installed profiles: %@", type: .info , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n"))
|
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||||
|
|
||||||
let response = InstallProvisioningProfilesResponse()
|
let response = InstallProvisioningProfilesResponse()
|
||||||
completionHandler(.success(response))
|
completionHandler(.success(response))
|
||||||
@@ -78,16 +84,18 @@ struct DaemonRequestHandler: RequestHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for _: Connection,
|
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
||||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void) {
|
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
||||||
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { result in
|
{
|
||||||
switch result {
|
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
|
||||||
case let .failure(error):
|
switch result
|
||||||
os_log("Failed to remove profiles %@ : %@", type: .error, request.bundleIdentifiers, error.localizedDescription)
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
|
|
||||||
case .success:
|
case .success:
|
||||||
os_log("Removed profiles: %@", type: .info , request.bundleIdentifiers)
|
print("Removed profiles:", request.bundleIdentifiers)
|
||||||
|
|
||||||
let response = RemoveProvisioningProfilesResponse()
|
let response = RemoveProvisioningProfilesResponse()
|
||||||
completionHandler(.success(response))
|
completionHandler(.success(response))
|
||||||
@@ -95,15 +103,17 @@ struct DaemonRequestHandler: RequestHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for _: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void) {
|
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
||||||
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { result in
|
{
|
||||||
switch result {
|
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
|
||||||
case let .failure(error):
|
switch result
|
||||||
os_log("Failed to remove app %@ : %@", type: .error , request.bundleIdentifier, error.localizedDescription)
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
|
|
||||||
case .success:
|
case .success:
|
||||||
os_log("Removed app: %@", type: .info , request.bundleIdentifier)
|
print("Removed app:", request.bundleIdentifier)
|
||||||
|
|
||||||
let response = RemoveAppResponse()
|
let response = RemoveAppResponse()
|
||||||
completionHandler(.success(response))
|
completionHandler(.success(response))
|
||||||
@@ -8,41 +8,49 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Security
|
import Security
|
||||||
import SideStoreCore
|
|
||||||
|
|
||||||
class XPCConnectionHandler: NSObject, ConnectionHandler {
|
class XPCConnectionHandler: NSObject, ConnectionHandler
|
||||||
|
{
|
||||||
var connectionHandler: ((Connection) -> Void)?
|
var connectionHandler: ((Connection) -> Void)?
|
||||||
var disconnectionHandler: ((Connection) -> Void)?
|
var disconnectionHandler: ((Connection) -> Void)?
|
||||||
|
|
||||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
|
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
|
||||||
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
|
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
|
||||||
|
|
||||||
deinit {
|
deinit
|
||||||
|
{
|
||||||
self.stopListening()
|
self.stopListening()
|
||||||
}
|
}
|
||||||
|
|
||||||
func startListening() {
|
func startListening()
|
||||||
for listener in listeners {
|
{
|
||||||
|
for listener in self.listeners
|
||||||
|
{
|
||||||
listener.delegate = self
|
listener.delegate = self
|
||||||
listener.resume()
|
listener.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopListening() {
|
func stopListening()
|
||||||
listeners.forEach { $0.suspend() }
|
{
|
||||||
|
self.listeners.forEach { $0.suspend() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension XPCConnectionHandler {
|
private extension XPCConnectionHandler
|
||||||
func disconnect(_ connection: Connection) {
|
{
|
||||||
|
func disconnect(_ connection: Connection)
|
||||||
|
{
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
|
|
||||||
disconnectionHandler?(connection)
|
self.disconnectionHandler?(connection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension XPCConnectionHandler: NSXPCListenerDelegate {
|
extension XPCConnectionHandler: NSXPCListenerDelegate
|
||||||
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
{
|
||||||
|
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
|
||||||
|
{
|
||||||
let maximumPathLength = 4 * UInt32(MAXPATHLEN)
|
let maximumPathLength = 4 * UInt32(MAXPATHLEN)
|
||||||
|
|
||||||
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
|
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
|
||||||
@@ -78,7 +86,7 @@ extension XPCConnectionHandler: NSXPCListenerDelegate {
|
|||||||
self.disconnect(connection)
|
self.disconnect(connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionHandler?(connection)
|
self.connectionHandler?(connection)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
48
AltServer/ErrorDetailsViewController.swift
Normal file
48
AltServer/ErrorDetailsViewController.swift
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//
|
||||||
|
// ErrorDetailsViewController.swift
|
||||||
|
// AltServer
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 10/4/22.
|
||||||
|
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
class ErrorDetailsViewController: NSViewController
|
||||||
|
{
|
||||||
|
var error: NSError? {
|
||||||
|
didSet {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet private var errorCodeLabel: NSTextField!
|
||||||
|
@IBOutlet private var detailedDescriptionLabel: NSTextField!
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.detailedDescriptionLabel.preferredMaxLayoutWidth = 800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ErrorDetailsViewController
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
if !self.isViewLoaded
|
||||||
|
{
|
||||||
|
self.loadView()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let error = self.error else { return }
|
||||||
|
|
||||||
|
self.errorCodeLabel.stringValue = error.localizedErrorCode
|
||||||
|
|
||||||
|
let font = self.detailedDescriptionLabel.font ?? NSFont.systemFont(ofSize: 12)
|
||||||
|
let detailedDescription = error.formattedDetailedDescription(with: font)
|
||||||
|
self.detailedDescriptionLabel.attributedStringValue = detailedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
3
AltStore.xcconfig
Normal file
3
AltStore.xcconfig
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#include "Build.xcconfig"
|
||||||
|
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@
|
|||||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||||
BuildableName = "libAltKit.a"
|
BuildableName = "libAltKit.a"
|
||||||
BlueprintName = "AltKit"
|
BlueprintName = "AltKit"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||||
BuildableName = "SideDaemon"
|
BuildableName = "AltDaemon"
|
||||||
BlueprintName = "SideDaemon"
|
BlueprintName = "AltDaemon"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@@ -72,9 +72,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||||
BuildableName = "SideDaemon"
|
BuildableName = "AltDaemon"
|
||||||
BlueprintName = "SideDaemon"
|
BlueprintName = "AltDaemon"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
<EnvironmentVariables>
|
<EnvironmentVariables>
|
||||||
@@ -95,9 +95,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||||
BuildableName = "SideDaemon"
|
BuildableName = "AltDaemon"
|
||||||
BlueprintName = "SideDaemon"
|
BlueprintName = "AltDaemon"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
||||||
BuildableName = "AltPlugin.mailbundle"
|
BuildableName = "AltPlugin.mailbundle"
|
||||||
BlueprintName = "AltPlugin"
|
BlueprintName = "AltPlugin"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
||||||
BuildableName = "AltPlugin.mailbundle"
|
BuildableName = "AltPlugin.mailbundle"
|
||||||
BlueprintName = "AltPlugin"
|
BlueprintName = "AltPlugin"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
BuildableName = "AltServer.app"
|
BuildableName = "AltServer.app"
|
||||||
BlueprintName = "AltServer"
|
BlueprintName = "AltServer"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@@ -27,17 +27,19 @@
|
|||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
BuildableName = "AltServer.app"
|
BuildableName = "AltServer.app"
|
||||||
BlueprintName = "AltServer"
|
BlueprintName = "AltServer"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
<Testables>
|
<AdditionalOptions>
|
||||||
</Testables>
|
</AdditionalOptions>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
@@ -56,9 +58,11 @@
|
|||||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
BuildableName = "AltServer.app"
|
BuildableName = "AltServer.app"
|
||||||
BlueprintName = "AltServer"
|
BlueprintName = "AltServer"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
@@ -73,7 +77,7 @@
|
|||||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
BuildableName = "AltServer.app"
|
BuildableName = "AltServer.app"
|
||||||
BlueprintName = "AltServer"
|
BlueprintName = "AltServer"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
BuildableName = "SideStore.app"
|
BuildableName = "SideStore.app"
|
||||||
BlueprintName = "SideStore"
|
BlueprintName = "SideStore"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
BuildableName = "SideStore.app"
|
BuildableName = "SideStore.app"
|
||||||
BlueprintName = "SideStore"
|
BlueprintName = "SideStore"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
<CommandLineArguments>
|
<CommandLineArguments>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
BuildableName = "SideStore.app"
|
BuildableName = "SideStore.app"
|
||||||
BlueprintName = "SideStore"
|
BlueprintName = "SideStore"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
BuildableName = "SideStore.app"
|
BuildableName = "SideStore.app"
|
||||||
BlueprintName = "SideStore"
|
BlueprintName = "SideStore"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
BuildableName = "SideStore.app"
|
BuildableName = "SideStore.app"
|
||||||
BlueprintName = "SideStore"
|
BlueprintName = "SideStore"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
<CommandLineArguments>
|
<CommandLineArguments>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
BuildableName = "SideStore.app"
|
BuildableName = "SideStore.app"
|
||||||
BlueprintName = "SideStore"
|
BlueprintName = "SideStore"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||||
BuildableName = "AltXPC.xpc"
|
BuildableName = "AltXPC.xpc"
|
||||||
BlueprintName = "AltXPC"
|
BlueprintName = "AltXPC"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
BuildableName = "AltServer.app"
|
BuildableName = "AltServer.app"
|
||||||
BlueprintName = "AltServer"
|
BlueprintName = "AltServer"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||||
BuildableName = "AltXPC.xpc"
|
BuildableName = "AltXPC.xpc"
|
||||||
BlueprintName = "AltXPC"
|
BlueprintName = "AltXPC"
|
||||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
8
AltStore/AltStore-Bridging-Header.h
Normal file
8
AltStore/AltStore-Bridging-Header.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "NSAttributedString+Markdown.h"
|
||||||
|
#import "ALTAppPatcher.h"
|
||||||
|
|
||||||
|
#include "fragmentzip.h"
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
<!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>
|
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||||
<string>development</string>
|
<true/>
|
||||||
|
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.developer.siri</key>
|
<key>com.apple.developer.siri</key>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import SideStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
import AppCenter
|
import AppCenter
|
||||||
import AppCenterAnalytics
|
import AppCenterAnalytics
|
||||||
@@ -16,8 +16,10 @@ import AppCenterCrashes
|
|||||||
|
|
||||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||||
|
|
||||||
public extension AnalyticsManager {
|
extension AnalyticsManager
|
||||||
enum EventProperty: String {
|
{
|
||||||
|
enum EventProperty: String
|
||||||
|
{
|
||||||
case name
|
case name
|
||||||
case bundleIdentifier
|
case bundleIdentifier
|
||||||
case developerName
|
case developerName
|
||||||
@@ -28,24 +30,27 @@ public extension AnalyticsManager {
|
|||||||
case sourceURL
|
case sourceURL
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Event {
|
enum Event
|
||||||
|
{
|
||||||
case installedApp(InstalledApp)
|
case installedApp(InstalledApp)
|
||||||
case updatedApp(InstalledApp)
|
case updatedApp(InstalledApp)
|
||||||
case refreshedApp(InstalledApp)
|
case refreshedApp(InstalledApp)
|
||||||
|
|
||||||
public var name: String {
|
var name: String {
|
||||||
switch self {
|
switch self
|
||||||
|
{
|
||||||
case .installedApp: return "installed_app"
|
case .installedApp: return "installed_app"
|
||||||
case .updatedApp: return "updated_app"
|
case .updatedApp: return "updated_app"
|
||||||
case .refreshedApp: return "refreshed_app"
|
case .refreshedApp: return "refreshed_app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var properties: [EventProperty: String] {
|
var properties: [EventProperty: String] {
|
||||||
let properties: [EventProperty: String?]
|
let properties: [EventProperty: String?]
|
||||||
|
|
||||||
switch self {
|
switch self
|
||||||
case let .installedApp(app), let .updatedApp(app), let .refreshedApp(app):
|
{
|
||||||
|
case .installedApp(let app), .updatedApp(let app), .refreshedApp(let app):
|
||||||
let appBundleURL = InstalledApp.fileURL(for: app)
|
let appBundleURL = InstalledApp.fileURL(for: app)
|
||||||
let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
|
let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
|
||||||
|
|
||||||
@@ -66,22 +71,28 @@ public extension AnalyticsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class AnalyticsManager {
|
final class AnalyticsManager
|
||||||
public static let shared = AnalyticsManager()
|
{
|
||||||
|
static let shared = AnalyticsManager()
|
||||||
|
|
||||||
private init() {}
|
private init()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension AnalyticsManager {
|
extension AnalyticsManager
|
||||||
func start() {
|
{
|
||||||
|
func start()
|
||||||
|
{
|
||||||
AppCenter.start(withAppSecret: appCenterAppSecret, services: [
|
AppCenter.start(withAppSecret: appCenterAppSecret, services: [
|
||||||
Analytics.self,
|
Analytics.self,
|
||||||
Crashes.self
|
Crashes.self
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func trackEvent(_ event: Event) {
|
func trackEvent(_ event: Event)
|
||||||
let properties = event.properties.reduce(into: [:]) { properties, item in
|
{
|
||||||
|
let properties = event.properties.reduce(into: [:]) { (properties, item) in
|
||||||
properties[item.key.rawValue] = item.value
|
properties[item.key.rawValue] = item.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,17 +8,15 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
import SideStoreCore
|
import AltStoreCore
|
||||||
import RoxasUIKit
|
import Roxas
|
||||||
import OSLog
|
|
||||||
#if canImport(Logging)
|
|
||||||
import Logging
|
|
||||||
#endif
|
|
||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
extension AppContentViewController {
|
extension AppContentViewController
|
||||||
private enum Row: Int, CaseIterable {
|
{
|
||||||
|
private enum Row: Int, CaseIterable
|
||||||
|
{
|
||||||
case subtitle
|
case subtitle
|
||||||
case screenshots
|
case screenshots
|
||||||
case description
|
case description
|
||||||
@@ -27,7 +25,8 @@ extension AppContentViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class AppContentViewController: UITableViewController {
|
final class AppContentViewController: UITableViewController
|
||||||
|
{
|
||||||
var app: StoreApp!
|
var app: StoreApp!
|
||||||
|
|
||||||
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
||||||
@@ -68,56 +67,62 @@ final class AppContentViewController: UITableViewController {
|
|||||||
return CGSize(width: itemWidth, height: itemHeight)
|
return CGSize(width: itemWidth, height: itemHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
tableView.contentInset.bottom = 20
|
self.tableView.contentInset.bottom = 20
|
||||||
|
|
||||||
screenshotsCollectionView.dataSource = screenshotsDataSource
|
self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
|
||||||
screenshotsCollectionView.prefetchDataSource = screenshotsDataSource
|
self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
|
||||||
|
|
||||||
permissionsCollectionView.dataSource = permissionsDataSource
|
self.permissionsCollectionView.dataSource = self.permissionsDataSource
|
||||||
|
|
||||||
subtitleLabel.text = app.subtitle
|
self.subtitleLabel.text = self.app.subtitle
|
||||||
descriptionTextView.text = app.localizedDescription
|
self.descriptionTextView.text = self.app.localizedDescription
|
||||||
|
|
||||||
if let version = app.latestVersion {
|
if let version = self.app.latestAvailableVersion
|
||||||
versionDescriptionTextView.text = version.localizedDescription
|
{
|
||||||
versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
self.versionDescriptionTextView.text = version.localizedDescription
|
||||||
versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: dateFormatter)
|
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
||||||
sizeLabel.text = byteCountFormatter.string(fromByteCount: version.size)
|
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
|
||||||
} else {
|
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
||||||
versionDescriptionTextView.text = nil
|
}
|
||||||
versionLabel.text = nil
|
else
|
||||||
versionDateLabel.text = nil
|
{
|
||||||
sizeLabel.text = byteCountFormatter.string(fromByteCount: 0)
|
self.versionDescriptionTextView.text = nil
|
||||||
|
self.versionLabel.text = nil
|
||||||
|
self.versionDateLabel.text = nil
|
||||||
|
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptionTextView.maximumNumberOfLines = 5
|
self.descriptionTextView.maximumNumberOfLines = 5
|
||||||
descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
|
|
||||||
versionDescriptionTextView.maximumNumberOfLines = 3
|
self.versionDescriptionTextView.maximumNumberOfLines = 3
|
||||||
versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews()
|
||||||
|
{
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
guard var size = preferredScreenshotSize else { return }
|
guard var size = self.preferredScreenshotSize else { return }
|
||||||
size.height = min(size.height, screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
|
size.height = min(size.height, self.screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
|
||||||
|
|
||||||
let layout = screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||||
layout.itemSize = size
|
layout.itemSize = size
|
||||||
}
|
}
|
||||||
|
|
||||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||||
|
{
|
||||||
guard segue.identifier == "showPermission" else { return }
|
guard segue.identifier == "showPermission" else { return }
|
||||||
|
|
||||||
guard let cell = sender as? UICollectionViewCell, let indexPath = permissionsCollectionView.indexPath(for: cell) else { return }
|
guard let cell = sender as? UICollectionViewCell, let indexPath = self.permissionsCollectionView.indexPath(for: cell) else { return }
|
||||||
|
|
||||||
let permission = permissionsDataSource.item(at: indexPath)
|
let permission = self.permissionsDataSource.item(at: indexPath)
|
||||||
|
|
||||||
let maximumWidth = view.bounds.width - 20
|
let maximumWidth = self.view.bounds.width - 20
|
||||||
|
|
||||||
let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController
|
let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController
|
||||||
permissionPopoverViewController.permission = permission
|
permissionPopoverViewController.permission = permission
|
||||||
@@ -128,48 +133,55 @@ final class AppContentViewController: UITableViewController {
|
|||||||
|
|
||||||
permissionPopoverViewController.popoverPresentationController?.delegate = self
|
permissionPopoverViewController.popoverPresentationController?.delegate = self
|
||||||
permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame
|
permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame
|
||||||
permissionPopoverViewController.popoverPresentationController?.sourceView = permissionsCollectionView
|
permissionPopoverViewController.popoverPresentationController?.sourceView = self.permissionsCollectionView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AppContentViewController {
|
private extension AppContentViewController
|
||||||
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> {
|
{
|
||||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: app.screenshotURLs as [NSURL])
|
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
||||||
dataSource.cellConfigurationHandler = { cell, _, _ in
|
{
|
||||||
|
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||||
let cell = cell as! ScreenshotCollectionViewCell
|
let cell = cell as! ScreenshotCollectionViewCell
|
||||||
cell.imageView.image = nil
|
cell.imageView.image = nil
|
||||||
cell.imageView.isIndicatingActivity = true
|
cell.imageView.isIndicatingActivity = true
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { imageURL, _, completionHandler in
|
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||||
RSTAsyncBlockOperation { operation in
|
return RSTAsyncBlockOperation() { (operation) in
|
||||||
let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
|
let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
|
||||||
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in
|
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
|
||||||
guard !operation.isCancelled else { return operation.finish() }
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
if let image = response?.image {
|
if let image = response?.image
|
||||||
|
{
|
||||||
completionHandler(image, nil)
|
completionHandler(image, nil)
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
completionHandler(nil, error)
|
completionHandler(nil, error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchCompletionHandler = { cell, image, _, error in
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
let cell = cell as! ScreenshotCollectionViewCell
|
let cell = cell as! ScreenshotCollectionViewCell
|
||||||
cell.imageView.isIndicatingActivity = false
|
cell.imageView.isIndicatingActivity = false
|
||||||
cell.imageView.image = image
|
cell.imageView.image = image
|
||||||
|
|
||||||
if let error = error {
|
if let error = error
|
||||||
os_log("Error loading image: %@", type: .error, error.localizedDescription)
|
{
|
||||||
|
print("Error loading image:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission> {
|
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
||||||
let dataSource = RSTArrayCollectionViewDataSource(items: app.permissions)
|
{
|
||||||
dataSource.cellConfigurationHandler = { cell, permission, _ in
|
let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions)
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
||||||
let cell = cell as! PermissionCollectionViewCell
|
let cell = cell as! PermissionCollectionViewCell
|
||||||
cell.button.setImage(permission.type.icon, for: .normal)
|
cell.button.setImage(permission.type.icon, for: .normal)
|
||||||
cell.button.tintColor = .label
|
cell.button.tintColor = .label
|
||||||
@@ -180,13 +192,16 @@ private extension AppContentViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AppContentViewController {
|
private extension AppContentViewController
|
||||||
@objc func toggleCollapsingSection(_ sender: UIButton) {
|
{
|
||||||
|
@objc func toggleCollapsingSection(_ sender: UIButton)
|
||||||
|
{
|
||||||
let indexPath: IndexPath
|
let indexPath: IndexPath
|
||||||
|
|
||||||
switch sender {
|
switch sender
|
||||||
case descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
{
|
||||||
case versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||||
|
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||||
default: return
|
default: return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,19 +212,23 @@ private extension AppContentViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppContentViewController {
|
extension AppContentViewController
|
||||||
override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt _: IndexPath) {
|
{
|
||||||
cell.tintColor = app.tintColor
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
||||||
|
{
|
||||||
|
cell.tintColor = self.app.tintColor
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||||
switch Row.allCases[indexPath.row] {
|
{
|
||||||
|
switch Row.allCases[indexPath.row]
|
||||||
|
{
|
||||||
case .screenshots:
|
case .screenshots:
|
||||||
guard let size = preferredScreenshotSize else { return 0.0 }
|
guard let size = self.preferredScreenshotSize else { return 0.0 }
|
||||||
return size.height
|
return size.height
|
||||||
|
|
||||||
case .permissions:
|
case .permissions:
|
||||||
guard !app.permissions.isEmpty else { return 0.0 }
|
guard !self.app.permissions.isEmpty else { return 0.0 }
|
||||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -218,8 +237,10 @@ extension AppContentViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppContentViewController: UIPopoverPresentationControllerDelegate {
|
extension AppContentViewController: UIPopoverPresentationControllerDelegate
|
||||||
func adaptivePresentationStyle(for _: UIPresentationController, traitCollection _: UITraitCollection) -> UIModalPresentationStyle {
|
{
|
||||||
.none
|
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
|
||||||
|
{
|
||||||
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,30 +8,33 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@objc
|
final class PermissionCollectionViewCell: UICollectionViewCell
|
||||||
final class PermissionCollectionViewCell: UICollectionViewCell {
|
{
|
||||||
@IBOutlet var button: UIButton!
|
@IBOutlet var button: UIButton!
|
||||||
@IBOutlet var textLabel: UILabel!
|
@IBOutlet var textLabel: UILabel!
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
button.layer.cornerRadius = button.bounds.midY
|
self.button.layer.cornerRadius = self.button.bounds.midY
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tintColorDidChange() {
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
super.tintColorDidChange()
|
super.tintColorDidChange()
|
||||||
|
|
||||||
button.backgroundColor = tintColor.withAlphaComponent(0.15)
|
self.button.backgroundColor = self.tintColor.withAlphaComponent(0.15)
|
||||||
textLabel.textColor = tintColor
|
self.textLabel.textColor = self.tintColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
final class AppContentTableViewCell: UITableViewCell
|
||||||
final class AppContentTableViewCell: UITableViewCell {
|
{
|
||||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
|
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||||
|
{
|
||||||
// Ensure cell is laid out so it will report correct size.
|
// Ensure cell is laid out so it will report correct size.
|
||||||
layoutIfNeeded()
|
self.layoutIfNeeded()
|
||||||
|
|
||||||
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
||||||
|
|
||||||
570
AltStore/App Detail/AppViewController.swift
Normal file
570
AltStore/App Detail/AppViewController.swift
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
//
|
||||||
|
// AppViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/22/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
final class AppViewController: UIViewController
|
||||||
|
{
|
||||||
|
var app: StoreApp!
|
||||||
|
|
||||||
|
private var contentViewController: AppContentViewController!
|
||||||
|
private var contentViewControllerShadowView: UIView!
|
||||||
|
|
||||||
|
private var blurAnimator: UIViewPropertyAnimator?
|
||||||
|
private var navigationBarAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
|
private var contentSizeObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
@IBOutlet private var scrollView: UIScrollView!
|
||||||
|
@IBOutlet private var contentView: UIView!
|
||||||
|
|
||||||
|
@IBOutlet private var bannerView: AppBannerView!
|
||||||
|
|
||||||
|
@IBOutlet private var backButton: UIButton!
|
||||||
|
@IBOutlet private var backButtonContainerView: UIVisualEffectView!
|
||||||
|
|
||||||
|
@IBOutlet private var backgroundAppIconImageView: UIImageView!
|
||||||
|
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
|
||||||
|
|
||||||
|
@IBOutlet private var navigationBarTitleView: UIView!
|
||||||
|
@IBOutlet private var navigationBarDownloadButton: PillButton!
|
||||||
|
@IBOutlet private var navigationBarAppIconImageView: UIImageView!
|
||||||
|
@IBOutlet private var navigationBarAppNameLabel: UILabel!
|
||||||
|
|
||||||
|
private var _shouldResetLayout = false
|
||||||
|
private var _backgroundBlurEffect: UIBlurEffect?
|
||||||
|
private var _backgroundBlurTintColor: UIColor?
|
||||||
|
|
||||||
|
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return _preferredStatusBarStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.navigationBarTitleView.sizeToFit()
|
||||||
|
self.navigationItem.titleView = self.navigationBarTitleView
|
||||||
|
|
||||||
|
self.contentViewControllerShadowView = UIView()
|
||||||
|
self.contentViewControllerShadowView.backgroundColor = .white
|
||||||
|
self.contentViewControllerShadowView.layer.cornerRadius = 38
|
||||||
|
self.contentViewControllerShadowView.layer.shadowColor = UIColor.black.cgColor
|
||||||
|
self.contentViewControllerShadowView.layer.shadowOffset = CGSize(width: 0, height: -1)
|
||||||
|
self.contentViewControllerShadowView.layer.shadowRadius = 10
|
||||||
|
self.contentViewControllerShadowView.layer.shadowOpacity = 0.3
|
||||||
|
self.contentViewController.view.superview?.insertSubview(self.contentViewControllerShadowView, at: 0)
|
||||||
|
|
||||||
|
self.contentView.addGestureRecognizer(self.scrollView.panGestureRecognizer)
|
||||||
|
|
||||||
|
self.contentViewController.view.layer.cornerRadius = 38
|
||||||
|
self.contentViewController.view.layer.masksToBounds = true
|
||||||
|
|
||||||
|
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
|
||||||
|
self.contentViewController.tableView.showsVerticalScrollIndicator = false
|
||||||
|
|
||||||
|
// Bring to front so the scroll indicators are visible.
|
||||||
|
self.view.bringSubviewToFront(self.scrollView)
|
||||||
|
self.scrollView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
|
||||||
|
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
|
||||||
|
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
||||||
|
self.bannerView.iconImageView.image = nil
|
||||||
|
self.bannerView.iconImageView.tintColor = self.app.tintColor
|
||||||
|
self.bannerView.button.tintColor = self.app.tintColor
|
||||||
|
self.bannerView.tintColor = self.app.tintColor
|
||||||
|
|
||||||
|
self.bannerView.configure(for: self.app)
|
||||||
|
self.bannerView.accessibilityTraits.remove(.button)
|
||||||
|
|
||||||
|
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||||
|
|
||||||
|
self.backButtonContainerView.tintColor = self.app.tintColor
|
||||||
|
|
||||||
|
self.navigationController?.navigationBar.tintColor = self.app.tintColor
|
||||||
|
self.navigationBarDownloadButton.tintColor = self.app.tintColor
|
||||||
|
self.navigationBarAppNameLabel.text = self.app.name
|
||||||
|
self.navigationBarAppIconImageView.tintColor = self.app.tintColor
|
||||||
|
|
||||||
|
self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { [weak self] (tableView, change) in
|
||||||
|
self?.view.setNeedsLayout()
|
||||||
|
self?.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||||
|
|
||||||
|
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
|
||||||
|
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
|
||||||
|
|
||||||
|
// Load Images
|
||||||
|
for imageView in [self.bannerView.iconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
|
||||||
|
{
|
||||||
|
imageView.isIndicatingActivity = true
|
||||||
|
|
||||||
|
Nuke.loadImage(with: self.app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] (response, error) in
|
||||||
|
if response?.image != nil
|
||||||
|
{
|
||||||
|
imageView?.isIndicatingActivity = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
self.prepareBlur()
|
||||||
|
|
||||||
|
// Update blur immediately.
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
|
||||||
|
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
||||||
|
self.hideNavigationBar()
|
||||||
|
}, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
|
// Guard against "dismissing" when presenting via 3D Touch pop.
|
||||||
|
guard self.navigationController != nil else { return }
|
||||||
|
|
||||||
|
// Store reference since self.navigationController will be nil after disappearing.
|
||||||
|
let navigationController = self.navigationController
|
||||||
|
navigationController?.navigationBar.barStyle = .default // Don't animate, or else status bar might appear messed-up.
|
||||||
|
|
||||||
|
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
||||||
|
self.showNavigationBar(for: navigationController)
|
||||||
|
}, completion: { (context) in
|
||||||
|
if !context.isCancelled
|
||||||
|
{
|
||||||
|
self.showNavigationBar(for: navigationController)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
if self.navigationController == nil
|
||||||
|
{
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||||
|
{
|
||||||
|
guard segue.identifier == "embedAppContentViewController" else { return }
|
||||||
|
|
||||||
|
self.contentViewController = segue.destination as? AppContentViewController
|
||||||
|
self.contentViewController.app = self.app
|
||||||
|
|
||||||
|
if #available(iOS 15, *)
|
||||||
|
{
|
||||||
|
// Fix navigation bar + tab bar appearance on iOS 15.
|
||||||
|
self.setContentScrollView(self.scrollView)
|
||||||
|
self.navigationItem.scrollEdgeAppearance = self.navigationController?.navigationBar.standardAppearance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews()
|
||||||
|
{
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
if self._shouldResetLayout
|
||||||
|
{
|
||||||
|
// Various events can cause UI to mess up, so reset affected components now.
|
||||||
|
|
||||||
|
if self.navigationController?.topViewController == self
|
||||||
|
{
|
||||||
|
self.hideNavigationBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prepareBlur()
|
||||||
|
|
||||||
|
// Reset navigation bar animation, and create a new one later in this method if necessary.
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
|
||||||
|
self._shouldResetLayout = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusBarHeight = self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
||||||
|
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||||
|
|
||||||
|
let inset = 12 as CGFloat
|
||||||
|
let padding = 20 as CGFloat
|
||||||
|
|
||||||
|
let backButtonSize = self.backButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
||||||
|
var backButtonFrame = CGRect(x: inset, y: statusBarHeight,
|
||||||
|
width: backButtonSize.width + 20, height: backButtonSize.height + 20)
|
||||||
|
|
||||||
|
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.bannerView.bounds.height)
|
||||||
|
var contentFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
|
||||||
|
var backgroundIconFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)
|
||||||
|
|
||||||
|
let minimumHeaderY = backButtonFrame.maxY + 8
|
||||||
|
|
||||||
|
let minimumContentY = minimumHeaderY + headerFrame.height + padding
|
||||||
|
let maximumContentY = self.view.bounds.width * 0.667
|
||||||
|
|
||||||
|
// A full blur is too much, so we reduce the visible blur by 0.3, resulting in 70% blur.
|
||||||
|
let minimumBlurFraction = 0.3 as CGFloat
|
||||||
|
|
||||||
|
contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y
|
||||||
|
headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height
|
||||||
|
|
||||||
|
// Stretch the app icon image to fill additional vertical space if necessary.
|
||||||
|
let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height)
|
||||||
|
backgroundIconFrame.size.height = height
|
||||||
|
|
||||||
|
let blurThreshold = 0 as CGFloat
|
||||||
|
if self.scrollView.contentOffset.y < blurThreshold
|
||||||
|
{
|
||||||
|
// Determine how much to lessen blur by.
|
||||||
|
|
||||||
|
let range = 75 as CGFloat
|
||||||
|
let difference = -self.scrollView.contentOffset.y
|
||||||
|
|
||||||
|
let fraction = min(difference, range) / range
|
||||||
|
|
||||||
|
let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction
|
||||||
|
self.blurAnimator?.fractionComplete = fractionComplete
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Set blur to default.
|
||||||
|
|
||||||
|
self.blurAnimator?.fractionComplete = minimumBlurFraction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate navigation bar.
|
||||||
|
let showNavigationBarThreshold = (maximumContentY - minimumContentY) + backButtonFrame.origin.y
|
||||||
|
if self.scrollView.contentOffset.y > showNavigationBarThreshold
|
||||||
|
{
|
||||||
|
if self.navigationBarAnimator == nil
|
||||||
|
{
|
||||||
|
self.prepareNavigationBarAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
let difference = self.scrollView.contentOffset.y - showNavigationBarThreshold
|
||||||
|
let range = (headerFrame.height + padding) - (self.navigationController?.navigationBar.bounds.height ?? self.view.safeAreaInsets.top)
|
||||||
|
|
||||||
|
let fractionComplete = min(difference, range) / range
|
||||||
|
self.navigationBarAnimator?.fractionComplete = fractionComplete
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
let beginMovingBackButtonThreshold = (maximumContentY - minimumContentY)
|
||||||
|
if self.scrollView.contentOffset.y > beginMovingBackButtonThreshold
|
||||||
|
{
|
||||||
|
let difference = self.scrollView.contentOffset.y - beginMovingBackButtonThreshold
|
||||||
|
backButtonFrame.origin.y -= difference
|
||||||
|
}
|
||||||
|
|
||||||
|
let pinContentToTopThreshold = maximumContentY
|
||||||
|
if self.scrollView.contentOffset.y > pinContentToTopThreshold
|
||||||
|
{
|
||||||
|
contentFrame.origin.y = 0
|
||||||
|
backgroundIconFrame.origin.y = 0
|
||||||
|
|
||||||
|
let difference = self.scrollView.contentOffset.y - pinContentToTopThreshold
|
||||||
|
self.contentViewController.tableView.contentOffset.y = difference
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Keep content table view's content offset at the top.
|
||||||
|
self.contentViewController.tableView.contentOffset.y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep background app icon centered in gap between top of content and top of screen.
|
||||||
|
backgroundIconFrame.origin.y = (contentFrame.origin.y / 2) - backgroundIconFrame.height / 2
|
||||||
|
|
||||||
|
// Set frames.
|
||||||
|
self.contentViewController.view.superview?.frame = contentFrame
|
||||||
|
self.bannerView.frame = headerFrame
|
||||||
|
self.backgroundAppIconImageView.frame = backgroundIconFrame
|
||||||
|
self.backgroundBlurView.frame = backgroundIconFrame
|
||||||
|
self.backButtonContainerView.frame = backButtonFrame
|
||||||
|
|
||||||
|
self.contentViewControllerShadowView.frame = self.contentViewController.view.frame
|
||||||
|
|
||||||
|
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
|
||||||
|
|
||||||
|
self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight
|
||||||
|
|
||||||
|
// Adjust content offset + size.
|
||||||
|
let contentOffset = self.scrollView.contentOffset
|
||||||
|
|
||||||
|
var contentSize = self.contentViewController.tableView.contentSize
|
||||||
|
contentSize.height += maximumContentY
|
||||||
|
|
||||||
|
self.scrollView.contentSize = contentSize
|
||||||
|
self.scrollView.contentOffset = contentOffset
|
||||||
|
|
||||||
|
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
|
||||||
|
{
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit
|
||||||
|
{
|
||||||
|
self.blurAnimator?.stopAnimation(true)
|
||||||
|
self.navigationBarAnimator?.stopAnimation(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppViewController
|
||||||
|
{
|
||||||
|
final class func makeAppViewController(app: StoreApp) -> AppViewController
|
||||||
|
{
|
||||||
|
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
|
let appViewController = storyboard.instantiateViewController(withIdentifier: "appViewController") as! AppViewController
|
||||||
|
appViewController.app = app
|
||||||
|
return appViewController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppViewController
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
for button in [self.bannerView.button!, self.navigationBarDownloadButton!]
|
||||||
|
{
|
||||||
|
button.tintColor = self.app.tintColor
|
||||||
|
button.isIndicatingActivity = false
|
||||||
|
|
||||||
|
if self.app.installedApp == nil
|
||||||
|
{
|
||||||
|
button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = AppManager.shared.installationProgress(for: self.app)
|
||||||
|
button.progress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
if let versionDate = self.app.latestAvailableVersion?.date, versionDate > Date()
|
||||||
|
{
|
||||||
|
self.bannerView.button.countdownDate = versionDate
|
||||||
|
self.navigationBarDownloadButton.countdownDate = versionDate
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.bannerView.button.countdownDate = nil
|
||||||
|
self.navigationBarDownloadButton.countdownDate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let barButtonItem = self.navigationItem.rightBarButtonItem
|
||||||
|
self.navigationItem.rightBarButtonItem = nil
|
||||||
|
self.navigationItem.rightBarButtonItem = barButtonItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func showNavigationBar(for navigationController: UINavigationController? = nil)
|
||||||
|
{
|
||||||
|
let navigationController = navigationController ?? self.navigationController
|
||||||
|
navigationController?.navigationBar.alpha = 1.0
|
||||||
|
navigationController?.navigationBar.tintColor = .altPrimary
|
||||||
|
navigationController?.navigationBar.setNeedsLayout()
|
||||||
|
|
||||||
|
if self.traitCollection.userInterfaceStyle == .dark
|
||||||
|
{
|
||||||
|
self._preferredStatusBarStyle = .lightContent
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self._preferredStatusBarStyle = .default
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideNavigationBar(for navigationController: UINavigationController? = nil)
|
||||||
|
{
|
||||||
|
let navigationController = navigationController ?? self.navigationController
|
||||||
|
navigationController?.navigationBar.alpha = 0.0
|
||||||
|
|
||||||
|
self._preferredStatusBarStyle = .lightContent
|
||||||
|
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareBlur()
|
||||||
|
{
|
||||||
|
if let animator = self.blurAnimator
|
||||||
|
{
|
||||||
|
animator.stopAnimation(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundBlurView.effect = self._backgroundBlurEffect
|
||||||
|
self.backgroundBlurView.contentView.backgroundColor = self._backgroundBlurTintColor
|
||||||
|
|
||||||
|
self.blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
||||||
|
self?.backgroundBlurView.effect = nil
|
||||||
|
self?.backgroundBlurView.contentView.backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
self.blurAnimator?.startAnimation()
|
||||||
|
self.blurAnimator?.pauseAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareNavigationBarAnimation()
|
||||||
|
{
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
|
||||||
|
self.navigationBarAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
||||||
|
self?.showNavigationBar()
|
||||||
|
self?.navigationController?.navigationBar.tintColor = self?.app.tintColor
|
||||||
|
self?.navigationController?.navigationBar.barTintColor = nil
|
||||||
|
self?.contentViewController.view.layer.cornerRadius = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.navigationBarAnimator?.startAnimation()
|
||||||
|
self.navigationBarAnimator?.pauseAnimation()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetNavigationBarAnimation()
|
||||||
|
{
|
||||||
|
self.navigationBarAnimator?.stopAnimation(true)
|
||||||
|
self.navigationBarAnimator = nil
|
||||||
|
|
||||||
|
self.hideNavigationBar()
|
||||||
|
|
||||||
|
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppViewController
|
||||||
|
{
|
||||||
|
@IBAction func popViewController(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
self.navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func performAppAction(_ sender: PillButton)
|
||||||
|
{
|
||||||
|
if let installedApp = self.app.installedApp
|
||||||
|
{
|
||||||
|
self.open(installedApp)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.downloadApp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadApp()
|
||||||
|
{
|
||||||
|
guard self.app.installedApp == nil else { return }
|
||||||
|
|
||||||
|
let group = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
_ = try result.get()
|
||||||
|
}
|
||||||
|
catch OperationError.cancelled
|
||||||
|
{
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let toastView = ToastView(error: error, opensLog: true)
|
||||||
|
toastView.show(in: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.bannerView.button.progress = nil
|
||||||
|
self.navigationBarDownloadButton.progress = nil
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bannerView.button.progress = group.progress
|
||||||
|
self.navigationBarDownloadButton.progress = group.progress
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(_ installedApp: InstalledApp)
|
||||||
|
{
|
||||||
|
UIApplication.shared.open(installedApp.openAppURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppViewController
|
||||||
|
{
|
||||||
|
@objc func didChangeApp(_ notification: Notification)
|
||||||
|
{
|
||||||
|
// Async so that AppManager.installationProgress(for:) is nil when we update.
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func willEnterForeground(_ notification: Notification)
|
||||||
|
{
|
||||||
|
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
||||||
|
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didBecomeActive(_ notification: Notification)
|
||||||
|
{
|
||||||
|
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
||||||
|
|
||||||
|
// Fixes Navigation Bar appearing after app becomes inactive -> active again.
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppViewController: UIScrollViewDelegate
|
||||||
|
{
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView)
|
||||||
|
{
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,18 +8,20 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
import SideStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
final class PermissionPopoverViewController: UIViewController {
|
final class PermissionPopoverViewController: UIViewController
|
||||||
|
{
|
||||||
var permission: AppPermission!
|
var permission: AppPermission!
|
||||||
|
|
||||||
@IBOutlet private var nameLabel: UILabel!
|
@IBOutlet private var nameLabel: UILabel!
|
||||||
@IBOutlet private var descriptionLabel: UILabel!
|
@IBOutlet private var descriptionLabel: UILabel!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
nameLabel.text = permission.type.localizedName
|
self.nameLabel.text = self.permission.type.localizedName
|
||||||
descriptionLabel.text = permission.usageDescription
|
self.descriptionLabel.text = self.permission.usageDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
248
AltStore/App IDs/AppIDsViewController.swift
Normal file
248
AltStore/App IDs/AppIDsViewController.swift
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
//
|
||||||
|
// AppIDsViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 1/27/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
final class AppIDsViewController: UICollectionViewController
|
||||||
|
{
|
||||||
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
|
||||||
|
private var didInitialFetch = false
|
||||||
|
private var isLoading = false {
|
||||||
|
didSet {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet var activityIndicatorBarButtonItem: UIBarButtonItem!
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.collectionView.dataSource = self.dataSource
|
||||||
|
|
||||||
|
self.activityIndicatorBarButtonItem.isIndicatingActivity = true
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
refreshControl.addTarget(self, action: #selector(AppIDsViewController.fetchAppIDs), for: .primaryActionTriggered)
|
||||||
|
self.collectionView.refreshControl = refreshControl
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if !self.didInitialFetch
|
||||||
|
{
|
||||||
|
self.fetchAppIDs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppIDsViewController
|
||||||
|
{
|
||||||
|
func makeDataSource() -> RSTFetchedResultsCollectionViewDataSource<AppID>
|
||||||
|
{
|
||||||
|
let fetchRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
|
||||||
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AppID.name, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \AppID.bundleIdentifier, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \AppID.expirationDate, ascending: true)]
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
|
||||||
|
if let team = DatabaseManager.shared.activeTeam()
|
||||||
|
{
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(AppID.team), team)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fetchRequest.predicate = NSPredicate(value: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataSource = RSTFetchedResultsCollectionViewDataSource<AppID>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
|
dataSource.proxy = self
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
|
||||||
|
let tintColor = UIColor.altPrimary
|
||||||
|
|
||||||
|
let cell = cell as! BannerCollectionViewCell
|
||||||
|
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
cell.tintColor = tintColor
|
||||||
|
|
||||||
|
cell.bannerView.iconImageView.isHidden = true
|
||||||
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
|
|
||||||
|
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
|
||||||
|
|
||||||
|
let attributedAccessibilityLabel = NSMutableAttributedString(string: appID.name + ". ")
|
||||||
|
|
||||||
|
if let expirationDate = appID.expirationDate
|
||||||
|
{
|
||||||
|
cell.bannerView.button.isHidden = false
|
||||||
|
cell.bannerView.button.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
cell.bannerView.buttonLabel.isHidden = false
|
||||||
|
|
||||||
|
let currentDate = Date()
|
||||||
|
|
||||||
|
let formatter = DateComponentsFormatter()
|
||||||
|
formatter.unitsStyle = .full
|
||||||
|
formatter.includesApproximationPhrase = false
|
||||||
|
formatter.includesTimeRemainingPhrase = false
|
||||||
|
formatter.allowedUnits = [.minute, .hour, .day]
|
||||||
|
formatter.maximumUnitCount = 1
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
cell.bannerView.button.isHidden = true
|
||||||
|
cell.bannerView.button.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
cell.bannerView.buttonLabel.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.bannerView.titleLabel.text = appID.name
|
||||||
|
cell.bannerView.subtitleLabel.text = appID.bundleIdentifier
|
||||||
|
cell.bannerView.subtitleLabel.numberOfLines = 2
|
||||||
|
|
||||||
|
let attributedBundleIdentifier = NSMutableAttributedString(string: appID.bundleIdentifier.lowercased(), attributes: [.accessibilitySpeechPunctuation: true])
|
||||||
|
|
||||||
|
if let team = appID.team, let range = attributedBundleIdentifier.string.range(of: team.identifier.lowercased()), #available(iOS 13, *)
|
||||||
|
{
|
||||||
|
// Prefer to speak the team ID one character at a time.
|
||||||
|
let nsRange = NSRange(range, in: attributedBundleIdentifier.string)
|
||||||
|
attributedBundleIdentifier.addAttributes([.accessibilitySpeechSpellOut: true], range: nsRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
attributedAccessibilityLabel.append(attributedBundleIdentifier)
|
||||||
|
cell.bannerView.accessibilityAttributedLabel = attributedAccessibilityLabel
|
||||||
|
|
||||||
|
// Make sure refresh button is correct size.
|
||||||
|
cell.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func fetchAppIDs()
|
||||||
|
{
|
||||||
|
guard !self.isLoading else { return }
|
||||||
|
self.isLoading = true
|
||||||
|
|
||||||
|
AppManager.shared.fetchAppIDs { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let (_, context) = try result.get()
|
||||||
|
try context.save()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
if !self.isLoading
|
||||||
|
{
|
||||||
|
self.collectionView.refreshControl?.endRefreshing()
|
||||||
|
self.activityIndicatorBarButtonItem.isIndicatingActivity = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
||||||
|
{
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
|
{
|
||||||
|
return CGSize(width: collectionView.bounds.width, height: 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
|
||||||
|
{
|
||||||
|
let indexPath = IndexPath(row: 0, section: section)
|
||||||
|
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
|
||||||
|
|
||||||
|
// Use this view to calculate the optimal size based on the collection view's width
|
||||||
|
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
|
||||||
|
withHorizontalFittingPriority: .required, // Width is fixed
|
||||||
|
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||||
|
{
|
||||||
|
return CGSize(width: collectionView.bounds.width, height: 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||||
|
{
|
||||||
|
switch kind
|
||||||
|
{
|
||||||
|
case UICollectionView.elementKindSectionHeader:
|
||||||
|
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! TextCollectionReusableView
|
||||||
|
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
|
if let activeTeam = DatabaseManager.shared.activeTeam(), activeTeam.type == .free
|
||||||
|
{
|
||||||
|
let text = NSLocalizedString("""
|
||||||
|
Each app and app extension installed with SideStore must register an App ID with Apple. Apple limits non-developer Apple IDs to 10 App IDs at a time.
|
||||||
|
|
||||||
|
**App IDs can't be deleted**, but they do expire after one week. SideStore will automatically renew App IDs for all active apps once they've expired.
|
||||||
|
""", comment: "")
|
||||||
|
|
||||||
|
let attributedText = NSAttributedString(markdownRepresentation: text, attributes: [.font: headerView.textLabel.font as Any])
|
||||||
|
headerView.textLabel.attributedText = attributedText
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
headerView.textLabel.text = NSLocalizedString("""
|
||||||
|
Each app and app extension installed with SideStore must register an App ID with Apple.
|
||||||
|
|
||||||
|
App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
|
||||||
|
""", comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerView
|
||||||
|
|
||||||
|
case UICollectionView.elementKindSectionFooter:
|
||||||
|
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! TextCollectionReusableView
|
||||||
|
|
||||||
|
let count = self.dataSource.itemCount
|
||||||
|
if count == 1
|
||||||
|
{
|
||||||
|
footerView.textLabel.text = NSLocalizedString("1 App ID", comment: "")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs", comment: ""), NSNumber(value: count))
|
||||||
|
}
|
||||||
|
|
||||||
|
return footerView
|
||||||
|
|
||||||
|
default: fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,23 +6,32 @@
|
|||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import AVFoundation
|
|
||||||
import Intents
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import OSLog
|
import AVFoundation
|
||||||
#if canImport(Logging)
|
import Intents
|
||||||
import Logging
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import SideStoreCore
|
import Roxas
|
||||||
import SideStoreAppKit
|
|
||||||
import EmotionalDamage
|
import EmotionalDamage
|
||||||
import RoxasUIKit
|
|
||||||
|
extension AppDelegate
|
||||||
|
{
|
||||||
|
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
|
||||||
|
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
|
||||||
|
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
|
||||||
|
|
||||||
|
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
|
||||||
|
|
||||||
|
static let importAppDeepLinkURLKey = "fileURL"
|
||||||
|
static let appBackupResultKey = "result"
|
||||||
|
static let addSourceDeepLinkURLKey = "sourceURL"
|
||||||
|
}
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
final class AppDelegate: SideStoreAppDelegate {
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
@@ -47,27 +56,32 @@ final class AppDelegate: SideStoreAppDelegate {
|
|||||||
return ViewAppIntentHandler()
|
return ViewAppIntentHandler()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||||
|
{
|
||||||
// Register default settings before doing anything else.
|
// Register default settings before doing anything else.
|
||||||
UserDefaults.registerDefaults()
|
UserDefaults.registerDefaults()
|
||||||
|
|
||||||
DatabaseManager.shared.start { error in
|
|
||||||
if let error = error {
|
|
||||||
os_log("Failed to start DatabaseManager. Error: %@", type: .error , error.localizedDescription)
|
DatabaseManager.shared.start { (error) in
|
||||||
} else {
|
if let error = error
|
||||||
os_log("Started DatabaseManager.", type: .info)
|
{
|
||||||
let transformer = ALTAppPermissionTypeTransformer()
|
print("Failed to start DatabaseManager. Error:", error as Any)
|
||||||
ValueTransformer.setValueTransformer(transformer, forName: NSValueTransformerName(rawValue: "ALTAppPermissionTypeTransformer"))
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
print("Started DatabaseManager.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalyticsManager.shared.start()
|
AnalyticsManager.shared.start()
|
||||||
|
|
||||||
setTintColor()
|
self.setTintColor()
|
||||||
|
|
||||||
SecureValueTransformer.register()
|
SecureValueTransformer.register()
|
||||||
|
|
||||||
if UserDefaults.standard.firstLaunch == nil {
|
if UserDefaults.standard.firstLaunch == nil
|
||||||
|
{
|
||||||
Keychain.shared.reset()
|
Keychain.shared.reset()
|
||||||
UserDefaults.standard.firstLaunch = Date()
|
UserDefaults.standard.firstLaunch = Date()
|
||||||
}
|
}
|
||||||
@@ -78,67 +92,81 @@ final class AppDelegate: SideStoreAppDelegate {
|
|||||||
UserDefaults.standard.isDebugModeEnabled = true
|
UserDefaults.standard.isDebugModeEnabled = true
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
prepareForBackgroundFetch()
|
self.prepareForBackgroundFetch()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidEnterBackground(_: UIApplication) {
|
func applicationDidEnterBackground(_ application: UIApplication)
|
||||||
|
{
|
||||||
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
||||||
|
|
||||||
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||||
|
|
||||||
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||||
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
|
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
|
||||||
switch result {
|
switch result
|
||||||
|
{
|
||||||
case .success: break
|
case .success: break
|
||||||
case let .failure(error): os_log("[ALTLog] Failed to purge logged errors before %@. %@", type: .error , midnightOneMonthAgo.debugDescription, error.localizedDescription)
|
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillEnterForeground(_: UIApplication) {
|
}
|
||||||
|
|
||||||
|
func applicationWillEnterForeground(_ application: UIApplication)
|
||||||
|
{
|
||||||
AppManager.shared.update()
|
AppManager.shared.update()
|
||||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||||
open(url)
|
{
|
||||||
|
return self.open(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_: UIApplication, handlerFor intent: INIntent) -> Any? {
|
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
|
||||||
|
{
|
||||||
guard #available(iOS 14, *) else { return nil }
|
guard #available(iOS 14, *) else { return nil }
|
||||||
|
|
||||||
switch intent {
|
switch intent
|
||||||
case is RefreshAllIntent: return intentHandler
|
{
|
||||||
case is ViewAppIntent: return viewAppIntentHandler
|
case is RefreshAllIntent: return self.intentHandler
|
||||||
|
case is ViewAppIntent: return self.viewAppIntentHandler
|
||||||
default: return nil
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 13, *)
|
@available(iOS 13, *)
|
||||||
extension AppDelegate {
|
extension AppDelegate
|
||||||
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
{
|
||||||
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
|
||||||
|
{
|
||||||
// Called when a new scene session is being created.
|
// Called when a new scene session is being created.
|
||||||
// Use this method to select a configuration to create the new scene with.
|
// Use this method to select a configuration to create the new scene with.
|
||||||
UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_: UIApplication, didDiscardSceneSessions _: Set<UISceneSession>) {
|
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
|
||||||
|
{
|
||||||
// Called when the user discards a scene session.
|
// Called when the user discards a scene session.
|
||||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AppDelegate {
|
private extension AppDelegate
|
||||||
func setTintColor() {
|
{
|
||||||
window?.tintColor = .altPrimary
|
func setTintColor()
|
||||||
|
{
|
||||||
|
self.window?.tintColor = .altPrimary
|
||||||
}
|
}
|
||||||
|
|
||||||
func open(_ url: URL) -> Bool {
|
func open(_ url: URL) -> Bool
|
||||||
if url.isFileURL {
|
{
|
||||||
|
if url.isFileURL
|
||||||
|
{
|
||||||
guard url.pathExtension.lowercased() == "ipa" else { return false }
|
guard url.pathExtension.lowercased() == "ipa" else { return false }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@@ -146,11 +174,14 @@ private extension AppDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||||
guard let host = components.host?.lowercased() else { return false }
|
guard let host = components.host?.lowercased() else { return false }
|
||||||
|
|
||||||
switch host {
|
switch host
|
||||||
|
{
|
||||||
case "patreon":
|
case "patreon":
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||||
@@ -161,7 +192,8 @@ private extension AppDelegate {
|
|||||||
case "appbackupresponse":
|
case "appbackupresponse":
|
||||||
let result: Result<Void, Error>
|
let result: Result<Void, Error>
|
||||||
|
|
||||||
switch url.path.lowercased() {
|
switch url.path.lowercased()
|
||||||
|
{
|
||||||
case "/success": result = .success(())
|
case "/success": result = .success(())
|
||||||
case "/failure":
|
case "/failure":
|
||||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
|
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
|
||||||
@@ -207,12 +239,14 @@ private extension AppDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppDelegate {
|
extension AppDelegate
|
||||||
private func prepareForBackgroundFetch() {
|
{
|
||||||
|
private func prepareForBackgroundFetch()
|
||||||
|
{
|
||||||
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
|
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
|
||||||
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
|
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
|
||||||
|
|
||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -220,22 +254,25 @@ extension AppDelegate {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||||
{
|
{
|
||||||
let tokenParts = deviceToken.map { data -> String in
|
let tokenParts = deviceToken.map { data -> String in
|
||||||
String(format: "%02.2hhx", data)
|
return String(format: "%02.2hhx", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = tokenParts.joined()
|
let token = tokenParts.joined()
|
||||||
os_log("Push Token: %@", type: .debug , token)
|
print("Push Token:", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||||
|
{
|
||||||
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||||
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification {
|
{
|
||||||
|
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
|
||||||
|
{
|
||||||
let threeHours: TimeInterval = 3 * 60 * 60
|
let threeHours: TimeInterval = 3 * 60 * 60
|
||||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
||||||
|
|
||||||
@@ -249,31 +286,38 @@ extension AppDelegate {
|
|||||||
UserDefaults.standard.presentedLaunchReminderNotification = true
|
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||||
}
|
}
|
||||||
|
|
||||||
BackgroundTaskManager.shared.performExtendedBackgroundTask { taskResult, taskCompletionHandler in
|
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
||||||
if let error = taskResult.error {
|
if let error = taskResult.error
|
||||||
os_log("Error starting extended background task. Aborting. %@", type: .error, error.localizedDescription)
|
{
|
||||||
|
print("Error starting extended background task. Aborting.", error)
|
||||||
backgroundFetchCompletionHandler(.failed)
|
backgroundFetchCompletionHandler(.failed)
|
||||||
taskCompletionHandler()
|
taskCompletionHandler()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !DatabaseManager.shared.isStarted {
|
if !DatabaseManager.shared.isStarted
|
||||||
DatabaseManager.shared.start { error in
|
{
|
||||||
if error != nil {
|
DatabaseManager.shared.start() { (error) in
|
||||||
|
if error != nil
|
||||||
|
{
|
||||||
backgroundFetchCompletionHandler(.failed)
|
backgroundFetchCompletionHandler(.failed)
|
||||||
taskCompletionHandler()
|
taskCompletionHandler()
|
||||||
} else {
|
}
|
||||||
self.performBackgroundFetch { backgroundFetchResult in
|
else
|
||||||
|
{
|
||||||
|
self.performBackgroundFetch { (backgroundFetchResult) in
|
||||||
backgroundFetchCompletionHandler(backgroundFetchResult)
|
backgroundFetchCompletionHandler(backgroundFetchResult)
|
||||||
} refreshAppsCompletionHandler: { _ in
|
} refreshAppsCompletionHandler: { (refreshAppsResult) in
|
||||||
taskCompletionHandler()
|
taskCompletionHandler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
self.performBackgroundFetch { backgroundFetchResult in
|
else
|
||||||
|
{
|
||||||
|
self.performBackgroundFetch { (backgroundFetchResult) in
|
||||||
backgroundFetchCompletionHandler(backgroundFetchResult)
|
backgroundFetchCompletionHandler(backgroundFetchResult)
|
||||||
} refreshAppsCompletionHandler: { _ in
|
} refreshAppsCompletionHandler: { (refreshAppsResult) in
|
||||||
taskCompletionHandler()
|
taskCompletionHandler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,31 +325,37 @@ extension AppDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||||
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) {
|
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||||
fetchSources { result in
|
{
|
||||||
switch result {
|
self.fetchSources { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||||
case .success: backgroundFetchCompletionHandler(.newData)
|
case .success: backgroundFetchCompletionHandler(.newData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !UserDefaults.standard.isBackgroundRefreshEnabled {
|
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||||
|
{
|
||||||
refreshAppsCompletionHandler(.success([:]))
|
refreshAppsCompletionHandler(.success([:]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
|
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||||
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
|
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AppDelegate {
|
private extension AppDelegate
|
||||||
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void) {
|
{
|
||||||
AppManager.shared.fetchSources { result in
|
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
|
||||||
do {
|
{
|
||||||
|
AppManager.shared.fetchSources() { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
let (sources, context) = try result.get()
|
let (sources, context) = try result.get()
|
||||||
|
|
||||||
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||||
@@ -329,9 +379,10 @@ private extension AppDelegate {
|
|||||||
let updates = try context.fetch(updatesFetchRequest)
|
let updates = try context.fetch(updatesFetchRequest)
|
||||||
let newsItems = try context.fetch(newsItemsFetchRequest)
|
let newsItems = try context.fetch(newsItemsFetchRequest)
|
||||||
|
|
||||||
for update in updates {
|
for update in updates
|
||||||
|
{
|
||||||
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
||||||
guard let storeApp = update.storeApp, 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: "")
|
||||||
@@ -342,15 +393,19 @@ private extension AppDelegate {
|
|||||||
UNUserNotificationCenter.current().add(request)
|
UNUserNotificationCenter.current().add(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
for newsItem in newsItems {
|
for newsItem in newsItems
|
||||||
|
{
|
||||||
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
|
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
|
||||||
guard !newsItem.isSilent else { continue }
|
guard !newsItem.isSilent else { continue }
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
if let app = newsItem.storeApp {
|
if let app = newsItem.storeApp
|
||||||
|
{
|
||||||
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
|
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
content.title = NSLocalizedString("SideStore News", comment: "")
|
content.title = NSLocalizedString("SideStore News", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,8 +421,10 @@ private extension AppDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
completionHandler(.success(sources))
|
completionHandler(.success(sources))
|
||||||
} catch {
|
}
|
||||||
os_log("Error fetching apps: %@", type: .error, error.localizedDescription)
|
catch
|
||||||
|
{
|
||||||
|
print("Error fetching apps:", error)
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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">
|
<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="retina6_12" 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="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"/>
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
<!--Navigation Controller-->
|
<!--Navigation Controller-->
|
||||||
<scene sceneID="lNR-II-WoW">
|
<scene sceneID="lNR-II-WoW">
|
||||||
<objects>
|
<objects>
|
||||||
<navigationController storyboardIdentifier="navigationController" useStoryboardIdentifierAsRestorationIdentifier="YES" 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="SideStore" 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="59" width="393" 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,34 +36,34 @@
|
|||||||
<!--Authentication View Controller-->
|
<!--Authentication View Controller-->
|
||||||
<scene sceneID="OCd-xc-Ms7">
|
<scene sceneID="OCd-xc-Ms7">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="authenticationViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="SideStoreAppKit" 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="393" height="852"/>
|
<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="103" width="393" height="715"/>
|
<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="393" height="852"/>
|
<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="393" height="715"/>
|
<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="361" height="359"/>
|
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="361" height="67"/>
|
<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="357.33333333333331" height="40.666666666666664"/>
|
<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"/>
|
||||||
</label>
|
</label>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
|
||||||
<rect key="frame" x="0.0" y="46.666666666666664" width="306.33333333333331" height="20.333333333333336"/>
|
<rect key="frame" x="0.0" y="47" width="306.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.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -71,19 +71,19 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf">
|
||||||
<rect key="frame" x="0.0" y="117" width="361" height="242"/>
|
<rect key="frame" x="0.0" y="117.5" width="343" height="242"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="361" height="159"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="159"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="361" height="72"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="72"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="APPLE ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="59N-O1-6bM">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="APPLE ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="59N-O1-6bM">
|
||||||
<rect key="frame" x="14" y="0.0" width="347" height="17"/>
|
<rect key="frame" x="14" y="0.0" width="329" height="17"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -92,10 +92,10 @@
|
|||||||
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
|
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
|
||||||
</stackView>
|
</stackView>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1">
|
||||||
<rect key="frame" x="0.0" y="21" width="361" height="51"/>
|
<rect key="frame" x="0.0" y="21" width="343" height="51"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo">
|
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo">
|
||||||
<rect key="frame" x="14" y="0.0" width="333" height="51"/>
|
<rect key="frame" x="14" y="0.0" width="315" height="51"/>
|
||||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
||||||
@@ -118,13 +118,13 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password">
|
||||||
<rect key="frame" x="0.0" y="87" width="361" height="72"/>
|
<rect key="frame" x="0.0" y="87" width="343" height="72"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PASSWORD" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ava-XY-7vs">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PASSWORD" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ava-XY-7vs">
|
||||||
<rect key="frame" x="14" y="0.0" width="347" height="17"/>
|
<rect key="frame" x="14" y="0.0" width="329" height="17"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -133,10 +133,10 @@
|
|||||||
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
|
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
|
||||||
</stackView>
|
</stackView>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5">
|
||||||
<rect key="frame" x="0.0" y="21" width="361" height="51"/>
|
<rect key="frame" x="0.0" y="21" width="343" height="51"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT">
|
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT">
|
||||||
<rect key="frame" x="14" y="0.0" width="333" height="51"/>
|
<rect key="frame" x="14" y="0.0" width="315" height="51"/>
|
||||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
|
||||||
<rect key="frame" x="0.0" y="191" width="361" height="51"/>
|
<rect key="frame" x="0.0" y="191" width="343" height="51"/>
|
||||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
|
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
|
||||||
@@ -179,16 +179,16 @@
|
|||||||
</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="611" width="361" height="96"/>
|
<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="361" height="20.333333333333332"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" 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>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY">
|
||||||
<rect key="frame" x="0.0" y="24.333333333333371" width="361" height="71.666666666666671"/>
|
<rect key="frame" x="0.0" y="24.5" width="343" height="72"/>
|
||||||
<string key="text">Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.</string>
|
<string key="text">Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.</string>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||||
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
@@ -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,19 +258,19 @@
|
|||||||
<!--How it works-->
|
<!--How it works-->
|
||||||
<scene sceneID="dMt-EA-SGy">
|
<scene sceneID="dMt-EA-SGy">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="SideStoreAppKit" 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="393" height="852"/>
|
<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="103" width="393" height="656"/>
|
<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.000000000000007" width="361" height="95.666666666666686"/>
|
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-3h-B8f">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-3h-B8f">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/>
|
<constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
@@ -279,16 +279,16 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0">
|
||||||
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/>
|
<rect key="frame" x="79" y="16" width="264" height="64"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/>
|
<rect key="frame" x="0.0" y="0.0" width="264" 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>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH">
|
||||||
<rect key="frame" x="0.0" y="25.333333333333346" width="282" height="38.333333333333343"/>
|
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||||
<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"/>
|
||||||
@@ -298,10 +298,10 @@
|
|||||||
</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="198.33333333333331" width="361" height="95.666666666666686"/>
|
<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.666666666666671"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/>
|
<constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
@@ -310,16 +310,16 @@
|
|||||||
<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="16.333333333333371" width="282" height="63"/>
|
<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="282" height="20.333333333333332"/>
|
<rect key="frame" x="0.0" y="0.0" width="264" 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>
|
||||||
<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.333333333333311" width="282" height="37.666666666666657"/>
|
<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,10 +329,10 @@
|
|||||||
</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="362" width="361" height="95.666666666666686"/>
|
<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.666666666666671"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/>
|
<constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
@@ -341,16 +341,16 @@
|
|||||||
<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="15.999999999999996" width="282" height="63.666666666666657"/>
|
<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="282" height="20.333333333333332"/>
|
<rect key="frame" x="0.0" y="0.0" width="264" 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>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt">
|
||||||
<rect key="frame" x="0.0" y="25.333333333333318" width="282" height="38.333333333333343"/>
|
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||||
<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"/>
|
||||||
@@ -360,10 +360,10 @@
|
|||||||
</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="525.33333333333337" width="361" height="95.666666666666629"/>
|
<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.666666666666671"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/>
|
<constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
@@ -372,16 +372,16 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
|
||||||
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/>
|
<rect key="frame" x="79" y="17" width="264" height="62"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/>
|
<rect key="frame" x="0.0" y="0.0" width="264" 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>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
|
||||||
<rect key="frame" x="0.0" y="25.333333333333261" width="282" height="38.333333333333343"/>
|
<rect key="frame" x="0.0" y="25.5" width="264" height="36.5"/>
|
||||||
<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"/>
|
||||||
@@ -394,7 +394,7 @@
|
|||||||
<edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/>
|
<edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/>
|
||||||
</stackView>
|
</stackView>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
|
||||||
<rect key="frame" x="16" y="759" width="361" height="51"/>
|
<rect key="frame" x="16" y="608" width="343" height="51"/>
|
||||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
|
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
|
||||||
@@ -431,22 +431,22 @@
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1353" y="736"/>
|
<point key="canvasLocation" x="1353" y="736"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Refresh SideStore-->
|
<!--Refresh AltStore-->
|
||||||
<scene sceneID="9Vh-dM-OqX">
|
<scene sceneID="9Vh-dM-OqX">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="refreshAltStoreViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStoreAppKit" 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="393" height="852"/>
|
<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 contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
</view>
|
</view>
|
||||||
<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="721" width="361" 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="SideStore" 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="361" 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>
|
||||||
<constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/>
|
<constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/>
|
||||||
@@ -461,7 +461,7 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
|
||||||
<rect key="frame" x="0.0" y="59" width="361" height="30"/>
|
<rect key="frame" x="0.0" y="59" width="343" height="30"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||||
<state key="normal" title="Refresh Later">
|
<state key="normal" title="Refresh Later">
|
||||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
@@ -485,7 +485,7 @@
|
|||||||
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
|
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<navigationItem key="navigationItem" title="Refresh SideStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
|
<navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
|
||||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
|
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
|
||||||
@@ -493,35 +493,35 @@
|
|||||||
</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" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="SideStoreAppKit" 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="393" height="852"/>
|
<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"/>
|
||||||
<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"/>
|
||||||
<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="SideStore" 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.333332061767578" width="393" 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="352.66666666666669" 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">
|
||||||
<rect key="frame" x="30.000000000000004" y="9.9999999999999982" width="56.333333333333336" height="20.333333333333332"/>
|
<rect key="frame" x="30" y="10" width="56.5" height="20.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<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>
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf">
|
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf">
|
||||||
<rect key="frame" x="30" y="33.333333333333329" width="70" height="14.333333333333334"/>
|
<rect key="frame" x="30" y="33.5" width="70" height="14.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
@@ -550,7 +550,7 @@
|
|||||||
</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"/>
|
||||||
169
AltStore/Authentication/AuthenticationViewController.swift
Normal file
169
AltStore/Authentication/AuthenticationViewController.swift
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
//
|
||||||
|
// AuthenticationViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 9/5/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
final class AuthenticationViewController: UIViewController
|
||||||
|
{
|
||||||
|
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
|
||||||
|
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
|
||||||
|
|
||||||
|
private weak var toastView: ToastView?
|
||||||
|
|
||||||
|
@IBOutlet private var appleIDTextField: UITextField!
|
||||||
|
@IBOutlet private var passwordTextField: UITextField!
|
||||||
|
@IBOutlet private var signInButton: UIButton!
|
||||||
|
|
||||||
|
@IBOutlet private var appleIDBackgroundView: UIView!
|
||||||
|
@IBOutlet private var passwordBackgroundView: UIView!
|
||||||
|
|
||||||
|
@IBOutlet private var scrollView: UIScrollView!
|
||||||
|
@IBOutlet private var contentStackView: UIStackView!
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.signInButton.activityIndicatorView.style = .medium
|
||||||
|
|
||||||
|
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
|
||||||
|
{
|
||||||
|
view.clipsToBounds = true
|
||||||
|
view.layer.cornerRadius = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
if UIScreen.main.isExtraCompactHeight
|
||||||
|
{
|
||||||
|
self.contentStackView.spacing = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationViewController.textFieldDidChangeText(_:)), name: UITextField.textDidChangeNotification, object: self.appleIDTextField)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationViewController.textFieldDidChangeText(_:)), name: UITextField.textDidChangeNotification, object: self.passwordTextField)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
self.signInButton.isIndicatingActivity = false
|
||||||
|
self.toastView?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AuthenticationViewController
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
if let _ = self.validate()
|
||||||
|
{
|
||||||
|
self.signInButton.isEnabled = true
|
||||||
|
self.signInButton.alpha = 1.0
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.signInButton.isEnabled = false
|
||||||
|
self.signInButton.alpha = 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate() -> (String, String)?
|
||||||
|
{
|
||||||
|
guard
|
||||||
|
let emailAddress = self.appleIDTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !emailAddress.isEmpty,
|
||||||
|
let password = self.passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !password.isEmpty
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
return (emailAddress, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AuthenticationViewController
|
||||||
|
{
|
||||||
|
@IBAction func authenticate()
|
||||||
|
{
|
||||||
|
guard let (emailAddress, password) = self.validate() else { return }
|
||||||
|
|
||||||
|
self.appleIDTextField.resignFirstResponder()
|
||||||
|
self.passwordTextField.resignFirstResponder()
|
||||||
|
|
||||||
|
self.signInButton.isIndicatingActivity = true
|
||||||
|
|
||||||
|
self.authenticationHandler?(emailAddress, password) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
||||||
|
// Ignore
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.signInButton.isIndicatingActivity = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error as NSError):
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: ""))
|
||||||
|
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
self.toastView = toastView
|
||||||
|
|
||||||
|
self.signInButton.isIndicatingActivity = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case .success((let account, let session)):
|
||||||
|
self.completionHandler?((account, session, password))
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.scrollView.setContentOffset(CGPoint(x: 0, y: -self.view.safeAreaInsets.top), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func cancel(_ sender: UIBarButtonItem)
|
||||||
|
{
|
||||||
|
self.completionHandler?(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthenticationViewController: UITextFieldDelegate
|
||||||
|
{
|
||||||
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool
|
||||||
|
{
|
||||||
|
switch textField
|
||||||
|
{
|
||||||
|
case self.appleIDTextField: self.passwordTextField.becomeFirstResponder()
|
||||||
|
case self.passwordTextField: self.authenticate()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFieldDidBeginEditing(_ textField: UITextField)
|
||||||
|
{
|
||||||
|
guard UIScreen.main.isExtraCompactHeight else { return }
|
||||||
|
|
||||||
|
// Position all the controls within visible frame.
|
||||||
|
var contentOffset = self.scrollView.contentOffset
|
||||||
|
contentOffset.y = 44
|
||||||
|
self.scrollView.setContentOffset(contentOffset, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthenticationViewController
|
||||||
|
{
|
||||||
|
@objc func textFieldDidChangeText(_ notification: Notification)
|
||||||
|
{
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
54
AltStore/Authentication/InstructionsViewController.swift
Normal file
54
AltStore/Authentication/InstructionsViewController.swift
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// InstructionsViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 9/6/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class InstructionsViewController: UIViewController
|
||||||
|
{
|
||||||
|
var completionHandler: (() -> Void)?
|
||||||
|
|
||||||
|
var showsBottomButton: Bool = false
|
||||||
|
|
||||||
|
@IBOutlet private var contentStackView: UIStackView!
|
||||||
|
@IBOutlet private var dismissButton: UIButton!
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .lightContent
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
if UIScreen.main.isExtraCompactHeight
|
||||||
|
{
|
||||||
|
self.contentStackView.layoutMargins.top = 0
|
||||||
|
self.contentStackView.layoutMargins.bottom = self.contentStackView.layoutMargins.left
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dismissButton.clipsToBounds = true
|
||||||
|
self.dismissButton.layer.cornerRadius = 16
|
||||||
|
|
||||||
|
if self.showsBottomButton
|
||||||
|
{
|
||||||
|
self.navigationItem.hidesBackButton = true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.dismissButton.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension InstructionsViewController
|
||||||
|
{
|
||||||
|
@IBAction func dismiss()
|
||||||
|
{
|
||||||
|
self.completionHandler?()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,54 +8,61 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import SideStoreCore
|
import Roxas
|
||||||
import RoxasUIKit
|
|
||||||
|
|
||||||
final class RefreshAltStoreViewController: UIViewController {
|
final class RefreshAltStoreViewController: UIViewController
|
||||||
|
{
|
||||||
var context: AuthenticatedOperationContext!
|
var context: AuthenticatedOperationContext!
|
||||||
|
|
||||||
var completionHandler: ((Result<Void, Error>) -> Void)?
|
var completionHandler: ((Result<Void, Error>) -> Void)?
|
||||||
|
|
||||||
@IBOutlet private var placeholderView: RSTPlaceholderView!
|
@IBOutlet private var placeholderView: RSTPlaceholderView!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
placeholderView.textLabel.isHidden = true
|
self.placeholderView.textLabel.isHidden = true
|
||||||
|
|
||||||
placeholderView.detailTextLabel.textAlignment = .left
|
self.placeholderView.detailTextLabel.textAlignment = .left
|
||||||
placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||||
placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "")
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension RefreshAltStoreViewController {
|
private extension RefreshAltStoreViewController
|
||||||
@IBAction func refreshAltStore(_ sender: PillButton) {
|
{
|
||||||
|
@IBAction func refreshAltStore(_ sender: PillButton)
|
||||||
|
{
|
||||||
guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return }
|
guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return }
|
||||||
|
|
||||||
func refresh() {
|
func refresh()
|
||||||
|
{
|
||||||
sender.isIndicatingActivity = true
|
sender.isIndicatingActivity = true
|
||||||
|
|
||||||
if let progress = AppManager.shared.installationProgress(for: altStore) {
|
if let progress = AppManager.shared.installationProgress(for: altStore)
|
||||||
|
{
|
||||||
// Cancel pending AltStore installation so we can start a new one.
|
// Cancel pending AltStore installation so we can start a new one.
|
||||||
progress.cancel()
|
progress.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
|
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
|
||||||
let group = AppManager.shared.install(altStore, presentingViewController: self, context: context) { result in
|
let group = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
|
||||||
switch result {
|
switch result
|
||||||
|
{
|
||||||
case .success: self.completionHandler?(.success(()))
|
case .success: self.completionHandler?(.success(()))
|
||||||
case let .failure(error as NSError):
|
case .failure(let error as NSError):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
sender.progress = nil
|
sender.progress = nil
|
||||||
sender.isIndicatingActivity = false
|
sender.isIndicatingActivity = false
|
||||||
|
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
|
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { _ in
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in
|
||||||
refresh()
|
refresh()
|
||||||
}))
|
}))
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { _ in
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { (action) in
|
||||||
self.completionHandler?(.failure(error))
|
self.completionHandler?(.failure(error))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -70,7 +77,8 @@ private extension RefreshAltStoreViewController {
|
|||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func cancel(_: UIButton) {
|
@IBAction func cancel(_ sender: UIButton)
|
||||||
completionHandler?(.failure(OperationError.cancelled))
|
{
|
||||||
|
self.completionHandler?(.failure(OperationError.cancelled))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
61
AltStore/Authentication/SelectTeamViewController.swift
Normal file
61
AltStore/Authentication/SelectTeamViewController.swift
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// SelectTeamViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Megarushing on 4/26/21.
|
||||||
|
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SafariServices
|
||||||
|
import MessageUI
|
||||||
|
import Intents
|
||||||
|
import IntentsUI
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
final class SelectTeamViewController: UITableViewController
|
||||||
|
{
|
||||||
|
public var teams: [ALTTeam]?
|
||||||
|
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?
|
||||||
|
|
||||||
|
private var prototypeHeaderFooterView: SettingsHeaderFooterView!
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .lightContent
|
||||||
|
}
|
||||||
|
|
||||||
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return teams?.count ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
return self.completionHandler!(.success((self.teams?[indexPath.row])!))
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "TeamCell", for: indexPath) as! InsetGroupTableViewCell
|
||||||
|
|
||||||
|
cell.textLabel?.text = self.teams?[indexPath.row].name
|
||||||
|
cell.detailTextLabel?.text = self.teams?[indexPath.row].type.localizedDescription
|
||||||
|
if indexPath.row == 0
|
||||||
|
{
|
||||||
|
cell.style = InsetGroupTableViewCell.Style.top
|
||||||
|
} else if indexPath.row == self.tableView(self.tableView, numberOfRowsInSection: indexPath.section) - 1 {
|
||||||
|
cell.style = InsetGroupTableViewCell.Style.bottom
|
||||||
|
} else {
|
||||||
|
cell.style = InsetGroupTableViewCell.Style.middle
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||||
|
"Teams"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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="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="21505"/>
|
<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"/>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<!--Launch View Controller-->
|
<!--Launch View Controller-->
|
||||||
<scene sceneID="q24-yd-v7v">
|
<scene sceneID="q24-yd-v7v">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="launchViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM">
|
<view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM">
|
||||||
<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"/>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<!--Tab Bar Controller-->
|
<!--Tab Bar Controller-->
|
||||||
<scene sceneID="yl2-sM-qoP">
|
<scene sceneID="yl2-sM-qoP">
|
||||||
<objects>
|
<objects>
|
||||||
<tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" useStoryboardIdentifierAsRestorationIdentifier="YES" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
|
<tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
|
||||||
<rect key="frame" x="0.0" y="975" width="768" height="49"/>
|
<rect key="frame" x="0.0" y="975" width="768" height="49"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<!--Browse-->
|
<!--Browse-->
|
||||||
<scene sceneID="rXq-UR-qQp">
|
<scene sceneID="rXq-UR-qQp">
|
||||||
<objects>
|
<objects>
|
||||||
<collectionViewController storyboardIdentifier="browseViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<collectionViewController id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
|
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
|
||||||
<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"/>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<!--App View Controller-->
|
<!--App View Controller-->
|
||||||
<scene sceneID="TgT-LO-3Er">
|
<scene sceneID="TgT-LO-3Er">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="appViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="appViewController" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" contentMode="scaleToFill" id="0cR-li-tCB">
|
<view key="view" contentMode="scaleToFill" id="0cR-li-tCB">
|
||||||
<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"/>
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg">
|
||||||
<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" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStoreAppKit">
|
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="37" y="287" width="300" height="93"/>
|
<rect key="frame" x="37" y="287" width="300" height="93"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
</view>
|
</view>
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS">
|
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS">
|
||||||
<barButtonItem key="rightBarButtonItem" style="done" id="FLf-DS-F77">
|
<barButtonItem key="rightBarButtonItem" style="plain" id="FLf-DS-F77">
|
||||||
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
|
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="287" y="6.5" width="72" height="31"/>
|
<rect key="frame" x="287" y="6.5" width="72" height="31"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
<!--App-->
|
<!--App-->
|
||||||
<scene sceneID="CgX-7h-sRI">
|
<scene sceneID="CgX-7h-sRI">
|
||||||
<objects>
|
<objects>
|
||||||
<tableViewController storyboardIdentifier="appContentViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<tableViewController id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU">
|
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU">
|
||||||
<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"/>
|
||||||
@@ -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"/>
|
||||||
@@ -511,7 +511,7 @@ World</string>
|
|||||||
<!--Permission Popover View Controller-->
|
<!--Permission Popover View Controller-->
|
||||||
<scene sceneID="24j-EJ-G4e">
|
<scene sceneID="24j-EJ-G4e">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="permissionPopoverViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<viewController id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX">
|
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="217"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="217"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
@@ -566,7 +566,7 @@ World</string>
|
|||||||
<!--News-->
|
<!--News-->
|
||||||
<scene sceneID="bqw-wB-hyB">
|
<scene sceneID="bqw-wB-hyB">
|
||||||
<objects>
|
<objects>
|
||||||
<collectionViewController storyboardIdentifier="newsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<collectionViewController id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef">
|
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef">
|
||||||
<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"/>
|
||||||
@@ -592,11 +592,11 @@ World</string>
|
|||||||
<!--Browse-->
|
<!--Browse-->
|
||||||
<scene sceneID="VHa-uP-bFU">
|
<scene sceneID="VHa-uP-bFU">
|
||||||
<objects>
|
<objects>
|
||||||
<navigationController storyboardIdentifier="forwardingNavigationControllerBrowse" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<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>
|
||||||
@@ -620,13 +620,13 @@ World</string>
|
|||||||
<!--My Apps-->
|
<!--My Apps-->
|
||||||
<scene sceneID="nhh-BJ-XiT">
|
<scene sceneID="nhh-BJ-XiT">
|
||||||
<objects>
|
<objects>
|
||||||
<navigationController storyboardIdentifier="forwardingNavigationControllerNyApps" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
|
<tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
|
||||||
<color key="badgeColor" name="Primary"/>
|
<color key="badgeColor" name="Primary"/>
|
||||||
</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"/>
|
||||||
@@ -641,7 +641,7 @@ World</string>
|
|||||||
<!--My Apps-->
|
<!--My Apps-->
|
||||||
<scene sceneID="EC8-Sf-AF9">
|
<scene sceneID="EC8-Sf-AF9">
|
||||||
<objects>
|
<objects>
|
||||||
<collectionViewController storyboardIdentifier="myAppsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<collectionViewController id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df">
|
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df">
|
||||||
<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"/>
|
||||||
@@ -660,7 +660,7 @@ World</string>
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStoreAppKit">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="8" y="0.0" width="359" height="60"/>
|
<rect key="frame" x="8" y="0.0" width="359" height="60"/>
|
||||||
</view>
|
</view>
|
||||||
</subviews>
|
</subviews>
|
||||||
@@ -797,7 +797,7 @@ World</string>
|
|||||||
<!--App IDs-->
|
<!--App IDs-->
|
||||||
<scene sceneID="kvf-US-rRe">
|
<scene sceneID="kvf-US-rRe">
|
||||||
<objects>
|
<objects>
|
||||||
<collectionViewController storyboardIdentifier="appIDsViewController" title="App IDs" useStoryboardIdentifierAsRestorationIdentifier="YES" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<collectionViewController title="App IDs" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
|
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
@@ -816,7 +816,7 @@ World</string>
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStoreAppKit">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
||||||
<accessibility key="accessibilityConfiguration">
|
<accessibility key="accessibilityConfiguration">
|
||||||
<bool key="isElement" value="YES"/>
|
<bool key="isElement" value="YES"/>
|
||||||
@@ -835,7 +835,7 @@ World</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</collectionViewCell>
|
</collectionViewCell>
|
||||||
</cells>
|
</cells>
|
||||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStoreAppKit">
|
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
@@ -856,7 +856,7 @@ World</string>
|
|||||||
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
|
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
|
||||||
</connections>
|
</connections>
|
||||||
</collectionReusableView>
|
</collectionReusableView>
|
||||||
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStoreAppKit">
|
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="170" width="375" height="50"/>
|
<rect key="frame" x="0.0" y="170" width="375" height="50"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
@@ -881,7 +881,7 @@ World</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</collectionView>
|
</collectionView>
|
||||||
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
|
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
|
||||||
<barButtonItem key="leftBarButtonItem" style="done" 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="7" 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"/>
|
||||||
@@ -905,11 +905,11 @@ World</string>
|
|||||||
<!--News-->
|
<!--News-->
|
||||||
<scene sceneID="BV8-6J-nIv">
|
<scene sceneID="BV8-6J-nIv">
|
||||||
<objects>
|
<objects>
|
||||||
<navigationController storyboardIdentifier="forwardingNavigationControllerNews" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<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>
|
||||||
@@ -925,7 +925,7 @@ World</string>
|
|||||||
<!--Navigation Controller-->
|
<!--Navigation Controller-->
|
||||||
<scene sceneID="1Gj-mS-BaN">
|
<scene sceneID="1Gj-mS-BaN">
|
||||||
<objects>
|
<objects>
|
||||||
<navigationController storyboardIdentifier="nav1" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" 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="108"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||||
@@ -943,7 +943,7 @@ World</string>
|
|||||||
<!--Sources-->
|
<!--Sources-->
|
||||||
<scene sceneID="0S1-zn-9KZ">
|
<scene sceneID="0S1-zn-9KZ">
|
||||||
<objects>
|
<objects>
|
||||||
<collectionViewController storyboardIdentifier="sourcesViewController" title="Sources" useStoryboardIdentifierAsRestorationIdentifier="YES" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
<collectionViewController title="Sources" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2">
|
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
@@ -962,7 +962,7 @@ World</string>
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStoreAppKit">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
||||||
<accessibility key="accessibilityConfiguration">
|
<accessibility key="accessibilityConfiguration">
|
||||||
<bool key="isElement" value="YES"/>
|
<bool key="isElement" value="YES"/>
|
||||||
@@ -981,7 +981,7 @@ World</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</collectionViewCell>
|
</collectionViewCell>
|
||||||
</cells>
|
</cells>
|
||||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStoreAppKit">
|
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
@@ -1067,7 +1067,7 @@ World</string>
|
|||||||
<!--Navigation Controller-->
|
<!--Navigation Controller-->
|
||||||
<scene sceneID="6NV-LQ-gKB">
|
<scene sceneID="6NV-LQ-gKB">
|
||||||
<objects>
|
<objects>
|
||||||
<navigationController storyboardIdentifier="nav2" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" 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="108"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||||
@@ -1095,7 +1095,7 @@ World</string>
|
|||||||
<image name="News" width="19" height="20"/>
|
<image name="News" width="19" height="20"/>
|
||||||
<image name="Settings" width="20" height="20"/>
|
<image name="Settings" width="20" height="20"/>
|
||||||
<namedColor name="Background">
|
<namedColor name="Background">
|
||||||
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.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.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
@@ -8,21 +8,17 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
import RoxasUIKit
|
import Roxas
|
||||||
import OSLog
|
|
||||||
#if canImport(Logging)
|
|
||||||
import Logging
|
|
||||||
#endif
|
|
||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
@objc final class BrowseCollectionViewCell: UICollectionViewCell {
|
@objc final class BrowseCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
var imageURLs: [URL] = [] {
|
var imageURLs: [URL] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
dataSource.items = imageURLs as [NSURL]
|
self.dataSource.items = self.imageURLs as [NSURL]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
|
||||||
@IBOutlet var bannerView: AppBannerView!
|
@IBOutlet var bannerView: AppBannerView!
|
||||||
@@ -30,49 +26,56 @@ import Nuke
|
|||||||
|
|
||||||
@IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
|
@IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
contentView.preservesSuperviewLayoutMargins = true
|
self.contentView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷♂️.
|
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷♂️.
|
||||||
screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||||
|
|
||||||
screenshotsCollectionView.delegate = self
|
self.screenshotsCollectionView.delegate = self
|
||||||
screenshotsCollectionView.dataSource = dataSource
|
self.screenshotsCollectionView.dataSource = self.dataSource
|
||||||
screenshotsCollectionView.prefetchDataSource = dataSource
|
self.screenshotsCollectionView.prefetchDataSource = self.dataSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension BrowseCollectionViewCell {
|
private extension BrowseCollectionViewCell
|
||||||
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> {
|
{
|
||||||
|
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
||||||
|
{
|
||||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: [])
|
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: [])
|
||||||
dataSource.cellConfigurationHandler = { cell, _, _ in
|
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||||
let cell = cell as! ScreenshotCollectionViewCell
|
let cell = cell as! ScreenshotCollectionViewCell
|
||||||
cell.imageView.image = nil
|
cell.imageView.image = nil
|
||||||
cell.imageView.isIndicatingActivity = true
|
cell.imageView.isIndicatingActivity = true
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { imageURL, _, completionHandler in
|
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||||
RSTAsyncBlockOperation { operation in
|
return RSTAsyncBlockOperation() { (operation) in
|
||||||
let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
|
let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
|
||||||
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in
|
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
|
||||||
guard !operation.isCancelled else { return operation.finish() }
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
if let image = response?.image {
|
if let image = response?.image
|
||||||
|
{
|
||||||
completionHandler(image, nil)
|
completionHandler(image, nil)
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
completionHandler(nil, error)
|
completionHandler(nil, error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchCompletionHandler = { cell, image, _, error in
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
let cell = cell as! ScreenshotCollectionViewCell
|
let cell = cell as! ScreenshotCollectionViewCell
|
||||||
cell.imageView.isIndicatingActivity = false
|
cell.imageView.isIndicatingActivity = false
|
||||||
cell.imageView.image = image
|
cell.imageView.image = image
|
||||||
|
|
||||||
if let error = error {
|
if let error = error
|
||||||
os_log("Error loading image: %@", type: .error , error.localizedDescription)
|
{
|
||||||
|
print("Error loading image:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,8 +83,10 @@ private extension BrowseCollectionViewCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout {
|
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout
|
||||||
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt _: IndexPath) -> CGSize {
|
{
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
|
{
|
||||||
// Assuming 9.0 / 16.0 ratio for now.
|
// Assuming 9.0 / 16.0 ratio for now.
|
||||||
let aspectRatio: CGFloat = 9.0 / 16.0
|
let aspectRatio: CGFloat = 9.0 / 16.0
|
||||||
|
|
||||||
@@ -1,28 +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="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" 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="21505"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.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"/>
|
||||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="browseCollectionViewCell" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="SideStoreAppKit">
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
|
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
|
||||||
<rect key="frame" x="16" y="0.0" width="343" height="369"/>
|
<rect key="frame" x="16" y="0.0" width="343" height="369"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="SideStoreAppKit">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
|
<constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
@@ -62,9 +61,4 @@
|
|||||||
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
|
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
|
||||||
</collectionViewCell>
|
</collectionViewCell>
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
|
||||||
<systemColor name="systemBackgroundColor">
|
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
</resources>
|
|
||||||
</document>
|
</document>
|
||||||
@@ -8,16 +8,14 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
import SideStoreCore
|
import minimuxer
|
||||||
import RoxasUIKit
|
import AltStoreCore
|
||||||
import OSLog
|
import Roxas
|
||||||
#if canImport(Logging)
|
|
||||||
import Logging
|
|
||||||
#endif
|
|
||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
class BrowseViewController: UICollectionViewController {
|
class BrowseViewController: UICollectionViewController
|
||||||
|
{
|
||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
||||||
|
|
||||||
@@ -25,7 +23,7 @@ class BrowseViewController: UICollectionViewController {
|
|||||||
|
|
||||||
private var loadingState: LoadingState = .loading {
|
private var loadingState: LoadingState = .loading {
|
||||||
didSet {
|
didSet {
|
||||||
update()
|
self.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,42 +31,47 @@ class BrowseViewController: UICollectionViewController {
|
|||||||
|
|
||||||
@IBOutlet private var sourcesBarButtonItem: UIBarButtonItem!
|
@IBOutlet private var sourcesBarButtonItem: UIBarButtonItem!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
#if BETA
|
#if BETA
|
||||||
dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
|
self.dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
|
||||||
navigationItem.searchController = dataSource.searchController
|
self.navigationItem.searchController = self.dataSource.searchController
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
self.collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||||
|
|
||||||
collectionView.dataSource = dataSource
|
self.collectionView.dataSource = self.dataSource
|
||||||
collectionView.prefetchDataSource = dataSource
|
self.collectionView.prefetchDataSource = self.dataSource
|
||||||
|
|
||||||
registerForPreviewing(with: self, sourceView: collectionView)
|
self.registerForPreviewing(with: self, sourceView: self.collectionView)
|
||||||
|
|
||||||
update()
|
self.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
fetchSource()
|
self.fetchSource()
|
||||||
updateDataSource()
|
self.updateDataSource()
|
||||||
|
|
||||||
update()
|
self.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction private func unwindFromSourcesViewController(_: UIStoryboardSegue) {
|
@IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
|
||||||
fetchSource()
|
{
|
||||||
|
self.fetchSource()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension BrowseViewController {
|
private extension BrowseViewController
|
||||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage> {
|
{
|
||||||
|
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
||||||
|
{
|
||||||
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
|
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
|
||||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
|
||||||
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
|
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
|
||||||
@@ -78,7 +81,7 @@ private extension BrowseViewController {
|
|||||||
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
|
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
|
|
||||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
dataSource.cellConfigurationHandler = { cell, app, _ in
|
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
||||||
let cell = cell as! BrowseCollectionViewCell
|
let cell = cell as! BrowseCollectionViewCell
|
||||||
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
|
||||||
@@ -101,7 +104,8 @@ private extension BrowseViewController {
|
|||||||
let tintColor = app.tintColor ?? .altPrimary
|
let tintColor = app.tintColor ?? .altPrimary
|
||||||
cell.tintColor = tintColor
|
cell.tintColor = tintColor
|
||||||
|
|
||||||
if app.installedApp == nil {
|
if app.installedApp == nil
|
||||||
|
{
|
||||||
let buttonTitle = NSLocalizedString("Free", comment: "")
|
let buttonTitle = NSLocalizedString("Free", comment: "")
|
||||||
cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
||||||
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name)
|
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name)
|
||||||
@@ -110,12 +114,17 @@ 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
|
{
|
||||||
} else {
|
cell.bannerView.button.countdownDate = versionDate
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
cell.bannerView.button.countdownDate = nil
|
cell.bannerView.button.countdownDate = nil
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||||
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name)
|
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name)
|
||||||
cell.bannerView.button.accessibilityValue = nil
|
cell.bannerView.button.accessibilityValue = nil
|
||||||
@@ -123,59 +132,72 @@ private extension BrowseViewController {
|
|||||||
cell.bannerView.button.countdownDate = nil
|
cell.bannerView.button.countdownDate = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { storeApp, _, completionHandler -> Foundation.Operation? in
|
dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
|
||||||
let iconURL = storeApp.iconURL
|
let iconURL = storeApp.iconURL
|
||||||
|
|
||||||
return RSTAsyncBlockOperation { operation in
|
return RSTAsyncBlockOperation() { (operation) in
|
||||||
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { response, error in
|
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
|
||||||
guard !operation.isCancelled else { return operation.finish() }
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
if let image = response?.image {
|
if let image = response?.image
|
||||||
|
{
|
||||||
completionHandler(image, nil)
|
completionHandler(image, nil)
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
completionHandler(nil, error)
|
completionHandler(nil, error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchCompletionHandler = { cell, image, _, error in
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
let cell = cell as! BrowseCollectionViewCell
|
let cell = cell as! BrowseCollectionViewCell
|
||||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||||
cell.bannerView.iconImageView.image = image
|
cell.bannerView.iconImageView.image = image
|
||||||
|
|
||||||
if let error = error {
|
if let error = error
|
||||||
os_log("Error loading image: %@", type: .error , error.localizedDescription)
|
{
|
||||||
|
print("Error loading image:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSource.placeholderView = placeholderView
|
dataSource.placeholderView = self.placeholderView
|
||||||
|
|
||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateDataSource() {
|
func updateDataSource()
|
||||||
dataSource.predicate = nil
|
{
|
||||||
|
self.dataSource.predicate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSource() {
|
func fetchSource()
|
||||||
loadingState = .loading
|
{
|
||||||
|
self.loadingState = .loading
|
||||||
|
|
||||||
AppManager.shared.fetchSources { result in
|
AppManager.shared.fetchSources() { (result) in
|
||||||
do {
|
do
|
||||||
do {
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
let (_, context) = try result.get()
|
let (_, context) = try result.get()
|
||||||
try context.save()
|
try context.save()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.loadingState = .finished(.success(()))
|
self.loadingState = .finished(.success(()))
|
||||||
}
|
}
|
||||||
} catch let error as AppManager.FetchSourcesError {
|
}
|
||||||
|
catch let error as AppManager.FetchSourcesError
|
||||||
|
{
|
||||||
try error.managedObjectContext?.save()
|
try error.managedObjectContext?.save()
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.dataSource.itemCount > 0 {
|
if self.dataSource.itemCount > 0
|
||||||
|
{
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
@@ -187,140 +209,163 @@ private extension BrowseViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update() {
|
func update()
|
||||||
switch loadingState {
|
{
|
||||||
|
switch self.loadingState
|
||||||
|
{
|
||||||
case .loading:
|
case .loading:
|
||||||
placeholderView.textLabel.isHidden = true
|
self.placeholderView.textLabel.isHidden = true
|
||||||
placeholderView.detailTextLabel.isHidden = false
|
self.placeholderView.detailTextLabel.isHidden = false
|
||||||
|
|
||||||
placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
|
||||||
|
|
||||||
placeholderView.activityIndicatorView.startAnimating()
|
self.placeholderView.activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
case let .finished(.failure(error)):
|
case .finished(.failure(let error)):
|
||||||
placeholderView.textLabel.isHidden = false
|
self.placeholderView.textLabel.isHidden = false
|
||||||
placeholderView.detailTextLabel.isHidden = false
|
self.placeholderView.detailTextLabel.isHidden = false
|
||||||
|
|
||||||
placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "")
|
self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "")
|
||||||
placeholderView.detailTextLabel.text = error.localizedDescription
|
self.placeholderView.detailTextLabel.text = error.localizedDescription
|
||||||
|
|
||||||
placeholderView.activityIndicatorView.stopAnimating()
|
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||||
|
|
||||||
case .finished(.success):
|
case .finished(.success):
|
||||||
placeholderView.textLabel.isHidden = true
|
self.placeholderView.textLabel.isHidden = true
|
||||||
placeholderView.detailTextLabel.isHidden = true
|
self.placeholderView.detailTextLabel.isHidden = true
|
||||||
|
|
||||||
placeholderView.activityIndicatorView.stopAnimating()
|
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension BrowseViewController {
|
private extension BrowseViewController
|
||||||
@IBAction func performAppAction(_ sender: PillButton) {
|
{
|
||||||
let point = collectionView.convert(sender.center, from: sender.superview)
|
@IBAction func performAppAction(_ sender: PillButton)
|
||||||
guard let indexPath = collectionView.indexPathForItem(at: point) else { return }
|
{
|
||||||
|
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||||
|
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
||||||
|
|
||||||
let app = dataSource.item(at: indexPath)
|
let app = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
if let installedApp = app.installedApp {
|
if let installedApp = app.installedApp
|
||||||
open(installedApp)
|
{
|
||||||
} else {
|
self.open(installedApp)
|
||||||
install(app, at: indexPath)
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.install(app, at: indexPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func install(_ app: StoreApp, at indexPath: IndexPath) {
|
func install(_ app: StoreApp, at indexPath: IndexPath)
|
||||||
|
{
|
||||||
let previousProgress = AppManager.shared.installationProgress(for: app)
|
let previousProgress = AppManager.shared.installationProgress(for: app)
|
||||||
guard previousProgress == nil else {
|
guard previousProgress == nil else {
|
||||||
previousProgress?.cancel()
|
previousProgress?.cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = AppManager.shared.install(app, presentingViewController: self) { result in
|
if !minimuxer.ready() {
|
||||||
|
let toastView = ToastView(error: MinimuxerError.NoConnection)
|
||||||
|
toastView.show(in: self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = 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 let .failure(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: os_log("Installed app: %@", type: .info , app.bundleIdentifier)
|
case .success: print("Installed app:", app.bundleIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.collectionView.reloadItems(at: [indexPath])
|
self.collectionView.reloadItems(at: [indexPath])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionView.reloadItems(at: [indexPath])
|
self.collectionView.reloadItems(at: [indexPath])
|
||||||
}
|
}
|
||||||
|
|
||||||
func open(_ installedApp: InstalledApp) {
|
func open(_ installedApp: InstalledApp)
|
||||||
|
{
|
||||||
UIApplication.shared.open(installedApp.openAppURL)
|
UIApplication.shared.open(installedApp.openAppURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BrowseViewController: UICollectionViewDelegateFlowLayout {
|
extension BrowseViewController: UICollectionViewDelegateFlowLayout
|
||||||
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
{
|
||||||
let item = dataSource.item(at: indexPath)
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
|
{
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
if let previousSize = cachedItemSizes[item.bundleIdentifier] {
|
if let previousSize = self.cachedItemSizes[item.bundleIdentifier]
|
||||||
|
{
|
||||||
return previousSize
|
return previousSize
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxVisibleScreenshots = 2 as CGFloat
|
let maxVisibleScreenshots = 2 as CGFloat
|
||||||
let aspectRatio: CGFloat = 16.0 / 9.0
|
let aspectRatio: CGFloat = 16.0 / 9.0
|
||||||
|
|
||||||
let layout = prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
let layout = self.prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||||
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
|
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
|
||||||
|
|
||||||
dataSource.cellConfigurationHandler(prototypeCell, item, indexPath)
|
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
|
||||||
|
|
||||||
let widthConstraint = prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||||
widthConstraint.isActive = true
|
widthConstraint.isActive = true
|
||||||
defer { widthConstraint.isActive = false }
|
defer { widthConstraint.isActive = false }
|
||||||
|
|
||||||
// Manually update cell width & layout so we can accurately calculate screenshot sizes.
|
// Manually update cell width & layout so we can accurately calculate screenshot sizes.
|
||||||
prototypeCell.frame.size.width = widthConstraint.constant
|
self.prototypeCell.frame.size.width = widthConstraint.constant
|
||||||
prototypeCell.layoutIfNeeded()
|
self.prototypeCell.layoutIfNeeded()
|
||||||
|
|
||||||
let collectionViewWidth = prototypeCell.screenshotsCollectionView.bounds.width
|
let collectionViewWidth = self.prototypeCell.screenshotsCollectionView.bounds.width
|
||||||
let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down)
|
let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down)
|
||||||
let screenshotHeight = screenshotWidth * aspectRatio
|
let screenshotHeight = screenshotWidth * aspectRatio
|
||||||
|
|
||||||
let heightConstraint = prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
|
let heightConstraint = self.prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
|
||||||
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
|
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
|
||||||
heightConstraint.isActive = true
|
heightConstraint.isActive = true
|
||||||
defer { heightConstraint.isActive = false }
|
defer { heightConstraint.isActive = false }
|
||||||
|
|
||||||
let itemSize = prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
let itemSize = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||||
cachedItemSizes[item.bundleIdentifier] = itemSize
|
self.cachedItemSizes[item.bundleIdentifier] = itemSize
|
||||||
return itemSize
|
return itemSize
|
||||||
}
|
}
|
||||||
|
|
||||||
override func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||||
let app = dataSource.item(at: indexPath)
|
{
|
||||||
|
let app = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
let appViewController = AppViewController.makeAppViewController(app: app)
|
let appViewController = AppViewController.makeAppViewController(app: app)
|
||||||
navigationController?.pushViewController(appViewController, animated: true)
|
self.navigationController?.pushViewController(appViewController, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BrowseViewController: UIViewControllerPreviewingDelegate {
|
extension BrowseViewController: UIViewControllerPreviewingDelegate
|
||||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
|
{
|
||||||
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
|
||||||
|
{
|
||||||
guard
|
guard
|
||||||
let indexPath = collectionView.indexPathForItem(at: location),
|
let indexPath = self.collectionView.indexPathForItem(at: location),
|
||||||
let cell = collectionView.cellForItem(at: indexPath)
|
let cell = self.collectionView.cellForItem(at: indexPath)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
previewingContext.sourceRect = cell.frame
|
previewingContext.sourceRect = cell.frame
|
||||||
|
|
||||||
let app = dataSource.item(at: indexPath)
|
let app = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
let appViewController = AppViewController.makeAppViewController(app: app)
|
let appViewController = AppViewController.makeAppViewController(app: app)
|
||||||
return appViewController
|
return appViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
func previewingContext(_: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
|
||||||
navigationController?.pushViewController(viewControllerToCommit, animated: true)
|
{
|
||||||
|
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
44
AltStore/Browse/ScreenshotCollectionViewCell.swift
Normal file
44
AltStore/Browse/ScreenshotCollectionViewCell.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// ScreenshotCollectionViewCell.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/15/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
@objc(ScreenshotCollectionViewCell)
|
||||||
|
class ScreenshotCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
let imageView = UIImageView(image: nil)
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
|
self.imageView.layer.masksToBounds = true
|
||||||
|
self.addSubview(self.imageView, pinningEdgesWith: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.imageView.layer.cornerRadius = 4
|
||||||
|
}
|
||||||
|
}
|
||||||
144
AltStore/Components/AppBannerView.swift
Normal file
144
AltStore/Components/AppBannerView.swift
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
//
|
||||||
|
// AppBannerView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/29/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
class AppBannerView: RSTNibView
|
||||||
|
{
|
||||||
|
override var accessibilityLabel: String? {
|
||||||
|
get { return self.accessibilityView?.accessibilityLabel }
|
||||||
|
set { self.accessibilityView?.accessibilityLabel = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
override open var accessibilityAttributedLabel: NSAttributedString? {
|
||||||
|
get { return self.accessibilityView?.accessibilityAttributedLabel }
|
||||||
|
set { self.accessibilityView?.accessibilityAttributedLabel = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
override var accessibilityValue: String? {
|
||||||
|
get { return self.accessibilityView?.accessibilityValue }
|
||||||
|
set { self.accessibilityView?.accessibilityValue = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
override open var accessibilityAttributedValue: NSAttributedString? {
|
||||||
|
get { return self.accessibilityView?.accessibilityAttributedValue }
|
||||||
|
set { self.accessibilityView?.accessibilityAttributedValue = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
override open var accessibilityTraits: UIAccessibilityTraits {
|
||||||
|
get { return self.accessibilityView?.accessibilityTraits ?? [] }
|
||||||
|
set { self.accessibilityView?.accessibilityTraits = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var originalTintColor: UIColor?
|
||||||
|
|
||||||
|
@IBOutlet var titleLabel: UILabel!
|
||||||
|
@IBOutlet var subtitleLabel: UILabel!
|
||||||
|
@IBOutlet var iconImageView: AppIconImageView!
|
||||||
|
@IBOutlet var button: PillButton!
|
||||||
|
@IBOutlet var buttonLabel: UILabel!
|
||||||
|
@IBOutlet var betaBadgeView: UIView!
|
||||||
|
|
||||||
|
@IBOutlet var backgroundEffectView: UIVisualEffectView!
|
||||||
|
|
||||||
|
@IBOutlet private var vibrancyView: UIVisualEffectView!
|
||||||
|
@IBOutlet private var accessibilityView: UIView!
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: coder)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
|
self.accessibilityView.accessibilityTraits.formUnion(.button)
|
||||||
|
|
||||||
|
self.isAccessibilityElement = false
|
||||||
|
self.accessibilityElements = [self.accessibilityView, self.button].compactMap { $0 }
|
||||||
|
|
||||||
|
self.betaBadgeView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
|
super.tintColorDidChange()
|
||||||
|
|
||||||
|
if self.tintAdjustmentMode != .dimmed
|
||||||
|
{
|
||||||
|
self.originalTintColor = self.tintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppBannerView
|
||||||
|
{
|
||||||
|
func configure(for app: AppProtocol)
|
||||||
|
{
|
||||||
|
struct AppValues
|
||||||
|
{
|
||||||
|
var name: String
|
||||||
|
var developerName: String? = nil
|
||||||
|
var isBeta: Bool = false
|
||||||
|
|
||||||
|
init(app: AppProtocol)
|
||||||
|
{
|
||||||
|
self.name = app.name
|
||||||
|
|
||||||
|
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
|
||||||
|
self.developerName = storeApp.developerName
|
||||||
|
|
||||||
|
if storeApp.isBeta
|
||||||
|
{
|
||||||
|
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
|
||||||
|
self.isBeta = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let values = AppValues(app: app)
|
||||||
|
self.titleLabel.text = app.name // Don't use values.name since that already includes "beta".
|
||||||
|
self.betaBadgeView.isHidden = !values.isBeta
|
||||||
|
|
||||||
|
if let developerName = values.developerName
|
||||||
|
{
|
||||||
|
self.subtitleLabel.text = developerName
|
||||||
|
self.accessibilityLabel = String(format: NSLocalizedString("%@ by %@", comment: ""), values.name, developerName)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.subtitleLabel.text = NSLocalizedString("Sideloaded", comment: "")
|
||||||
|
self.accessibilityLabel = values.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppBannerView
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.layer.cornerRadius = 22
|
||||||
|
|
||||||
|
self.subtitleLabel.textColor = self.originalTintColor ?? self.tintColor
|
||||||
|
self.backgroundEffectView.backgroundColor = self.originalTintColor ?? self.tintColor
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
<?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="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" 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="21505"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.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" customClass="AppBannerView" customModule="SideStoreAppKit">
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/>
|
<outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/>
|
||||||
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
|
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</placeholder>
|
</placeholder>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="appBannerView" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5">
|
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
|
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="SideStore" customModuleProvider="target">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="14" y="14" width="60" height="60"/>
|
<rect key="frame" x="14" y="14" width="60" height="60"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
|
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
|
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
|
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
|
||||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
43
AltStore/Components/AppIconImageView.swift
Normal file
43
AltStore/Components/AppIconImageView.swift
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// AppIconImageView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/9/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class AppIconImageView: UIImageView
|
||||||
|
{
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.contentMode = .scaleAspectFill
|
||||||
|
self.clipsToBounds = true
|
||||||
|
|
||||||
|
self.backgroundColor = .white
|
||||||
|
|
||||||
|
if #available(iOS 13, *)
|
||||||
|
{
|
||||||
|
self.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if self.layer.responds(to: Selector(("continuousCorners")))
|
||||||
|
{
|
||||||
|
self.layer.setValue(true, forKey: "continuousCorners")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
// Based off of 60pt icon having 12pt radius.
|
||||||
|
let radius = self.bounds.height / 5
|
||||||
|
self.layer.cornerRadius = radius
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,9 @@
|
|||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
public final class BackgroundTaskManager {
|
final class BackgroundTaskManager
|
||||||
public static let shared = BackgroundTaskManager()
|
{
|
||||||
|
static let shared = BackgroundTaskManager()
|
||||||
|
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
|
|
||||||
@@ -19,37 +20,45 @@ public final class BackgroundTaskManager {
|
|||||||
|
|
||||||
private let audioEngineQueue: DispatchQueue
|
private let audioEngineQueue: DispatchQueue
|
||||||
|
|
||||||
private init() {
|
private init()
|
||||||
audioEngine = AVAudioEngine()
|
{
|
||||||
audioEngine.mainMixerNode.outputVolume = 0.0
|
self.audioEngine = AVAudioEngine()
|
||||||
|
self.audioEngine.mainMixerNode.outputVolume = 0.0
|
||||||
|
|
||||||
player = AVAudioPlayerNode()
|
self.player = AVAudioPlayerNode()
|
||||||
audioEngine.attach(player)
|
self.audioEngine.attach(self.player)
|
||||||
|
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")!
|
let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")!
|
||||||
|
|
||||||
audioFile = try AVAudioFile(forReading: audioFileURL)
|
self.audioFile = try AVAudioFile(forReading: audioFileURL)
|
||||||
audioEngine.connect(player, to: audioEngine.mainMixerNode, format: audioFile.processingFormat)
|
self.audioEngine.connect(self.player, to: self.audioEngine.mainMixerNode, format: self.audioFile.processingFormat)
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
fatalError("Error. \(error)")
|
fatalError("Error. \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager")
|
self.audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension BackgroundTaskManager {
|
extension BackgroundTaskManager
|
||||||
func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void)) {
|
{
|
||||||
func finish() {
|
func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void))
|
||||||
player.stop()
|
{
|
||||||
audioEngine.stop()
|
func finish()
|
||||||
|
{
|
||||||
|
self.player.stop()
|
||||||
|
self.audioEngine.stop()
|
||||||
|
|
||||||
isPlaying = false
|
self.isPlaying = false
|
||||||
}
|
}
|
||||||
|
|
||||||
audioEngineQueue.sync {
|
self.audioEngineQueue.sync {
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
|
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
|
||||||
@@ -68,7 +77,9 @@ public extension BackgroundTaskManager {
|
|||||||
taskHandler(.success(())) {
|
taskHandler(.success(())) {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
taskHandler(.failure(error)) {
|
taskHandler(.failure(error)) {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
@@ -77,9 +88,11 @@ public extension BackgroundTaskManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension BackgroundTaskManager {
|
private extension BackgroundTaskManager
|
||||||
func scheduleAudioFile() {
|
{
|
||||||
player.scheduleFile(audioFile, at: nil) {
|
func scheduleAudioFile()
|
||||||
|
{
|
||||||
|
self.player.scheduleFile(self.audioFile, at: nil) {
|
||||||
self.audioEngineQueue.async {
|
self.audioEngineQueue.async {
|
||||||
guard self.isPlaying else { return }
|
guard self.isPlaying else { return }
|
||||||
self.scheduleAudioFile()
|
self.scheduleAudioFile()
|
||||||
@@ -8,18 +8,20 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@objc
|
final class BannerCollectionViewCell: UICollectionViewCell
|
||||||
final class BannerCollectionViewCell: UICollectionViewCell {
|
{
|
||||||
private(set) var errorBadge: UIView?
|
private(set) var errorBadge: UIView?
|
||||||
@IBOutlet private(set) var bannerView: AppBannerView!
|
@IBOutlet private(set) var bannerView: AppBannerView!
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
contentView.preservesSuperviewLayoutMargins = true
|
self.contentView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
if #available(iOS 13.0, *) {
|
if #available(iOS 13.0, *)
|
||||||
|
{
|
||||||
let errorBadge = UIView()
|
let errorBadge = UIView()
|
||||||
errorBadge.translatesAutoresizingMaskIntoConstraints = false
|
errorBadge.translatesAutoresizingMaskIntoConstraints = false
|
||||||
errorBadge.isHidden = true
|
errorBadge.isHidden = true
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class Button: UIButton {
|
final class Button: UIButton
|
||||||
|
{
|
||||||
override var intrinsicContentSize: CGSize {
|
override var intrinsicContentSize: CGSize {
|
||||||
var size = super.intrinsicContentSize
|
var size = super.intrinsicContentSize
|
||||||
size.width += 20
|
size.width += 20
|
||||||
@@ -16,21 +17,23 @@ final class Button: UIButton {
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
setTitleColor(.white, for: .normal)
|
self.setTitleColor(.white, for: .normal)
|
||||||
|
|
||||||
layer.masksToBounds = true
|
self.layer.masksToBounds = true
|
||||||
layer.cornerRadius = 8
|
self.layer.cornerRadius = 8
|
||||||
|
|
||||||
update()
|
self.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tintColorDidChange() {
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
super.tintColorDidChange()
|
super.tintColorDidChange()
|
||||||
|
|
||||||
update()
|
self.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
override var isHighlighted: Bool {
|
override var isHighlighted: Bool {
|
||||||
@@ -41,17 +44,22 @@ final class Button: UIButton {
|
|||||||
|
|
||||||
override var isEnabled: Bool {
|
override var isEnabled: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
update()
|
self.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Button {
|
private extension Button
|
||||||
func update() {
|
{
|
||||||
if isEnabled {
|
func update()
|
||||||
backgroundColor = tintColor
|
{
|
||||||
} else {
|
if self.isEnabled
|
||||||
backgroundColor = .lightGray
|
{
|
||||||
|
self.backgroundColor = self.tintColor
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.backgroundColor = .lightGray
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
150
AltStore/Components/CollapsingTextView.swift
Normal file
150
AltStore/Components/CollapsingTextView.swift
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
//
|
||||||
|
// CollapsingTextView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/23/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class CollapsingTextView: UITextView
|
||||||
|
{
|
||||||
|
var isCollapsed = true {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maximumNumberOfLines = 2 {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineSpacing: Double = 2 {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let moreButton = UIButton(type: .system)
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
|
if #available(iOS 16, *)
|
||||||
|
{
|
||||||
|
self.updateText()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.layoutManager.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
self.textContainerInset = .zero
|
||||||
|
self.textContainer.lineFragmentPadding = 0
|
||||||
|
self.textContainer.lineBreakMode = .byTruncatingTail
|
||||||
|
self.textContainer.heightTracksTextView = true
|
||||||
|
self.textContainer.widthTracksTextView = true
|
||||||
|
|
||||||
|
self.moreButton.setTitle(NSLocalizedString("More", comment: ""), for: .normal)
|
||||||
|
self.moreButton.addTarget(self, action: #selector(CollapsingTextView.toggleCollapsed(_:)), for: .primaryActionTriggered)
|
||||||
|
self.addSubview(self.moreButton)
|
||||||
|
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
guard let font = self.font else { return }
|
||||||
|
|
||||||
|
let buttonFont = UIFont.systemFont(ofSize: font.pointSize, weight: .medium)
|
||||||
|
self.moreButton.titleLabel?.font = buttonFont
|
||||||
|
|
||||||
|
let buttonY = (font.lineHeight + self.lineSpacing) * CGFloat(self.maximumNumberOfLines - 1)
|
||||||
|
let size = self.moreButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
||||||
|
|
||||||
|
let moreButtonFrame = CGRect(x: self.bounds.width - self.moreButton.bounds.width,
|
||||||
|
y: buttonY,
|
||||||
|
width: size.width,
|
||||||
|
height: font.lineHeight)
|
||||||
|
self.moreButton.frame = moreButtonFrame
|
||||||
|
|
||||||
|
if self.isCollapsed
|
||||||
|
{
|
||||||
|
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
||||||
|
|
||||||
|
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
|
||||||
|
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines)
|
||||||
|
|
||||||
|
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
||||||
|
{
|
||||||
|
var exclusionFrame = moreButtonFrame
|
||||||
|
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
||||||
|
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
|
||||||
|
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
|
||||||
|
|
||||||
|
self.moreButton.isHidden = false
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.textContainer.exclusionPaths = []
|
||||||
|
|
||||||
|
self.moreButton.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.textContainer.maximumNumberOfLines = 0
|
||||||
|
self.textContainer.exclusionPaths = []
|
||||||
|
|
||||||
|
self.moreButton.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.invalidateIntrinsicContentSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CollapsingTextView
|
||||||
|
{
|
||||||
|
@objc func toggleCollapsed(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat
|
||||||
|
{
|
||||||
|
return self.lineSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,13 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class ForwardingNavigationController: UINavigationController {
|
final class ForwardingNavigationController: UINavigationController
|
||||||
|
{
|
||||||
override var childForStatusBarStyle: UIViewController? {
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
self.topViewController
|
return self.topViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
override var childForStatusBarHidden: UIViewController? {
|
override var childForStatusBarHidden: UIViewController? {
|
||||||
topViewController
|
return self.topViewController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,29 +8,32 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
import RoxasUIKit
|
import Roxas
|
||||||
|
|
||||||
@objc
|
final class NavigationBar: UINavigationBar
|
||||||
final class NavigationBar: UINavigationBar {
|
{
|
||||||
@objc
|
|
||||||
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
|
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
|
||||||
|
|
||||||
private let backgroundColorView = UIView()
|
private let backgroundColorView = UIView()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
initialize()
|
self.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder)
|
||||||
|
{
|
||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
initialize()
|
self.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func initialize() {
|
private func initialize()
|
||||||
if #available(iOS 13, *) {
|
{
|
||||||
|
if #available(iOS 13, *)
|
||||||
|
{
|
||||||
let standardAppearance = UINavigationBarAppearance()
|
let standardAppearance = UINavigationBarAppearance()
|
||||||
standardAppearance.configureWithDefaultBackground()
|
standardAppearance.configureWithDefaultBackground()
|
||||||
standardAppearance.shadowColor = nil
|
standardAppearance.shadowColor = nil
|
||||||
@@ -40,7 +43,8 @@ final class NavigationBar: UINavigationBar {
|
|||||||
edgeAppearance.backgroundColor = self.barTintColor
|
edgeAppearance.backgroundColor = self.barTintColor
|
||||||
edgeAppearance.shadowColor = nil
|
edgeAppearance.shadowColor = nil
|
||||||
|
|
||||||
if let tintColor = self.barTintColor {
|
if let tintColor = self.barTintColor
|
||||||
|
{
|
||||||
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
|
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
|
||||||
|
|
||||||
standardAppearance.backgroundColor = tintColor
|
standardAppearance.backgroundColor = tintColor
|
||||||
@@ -49,37 +53,48 @@ final class NavigationBar: UINavigationBar {
|
|||||||
|
|
||||||
edgeAppearance.titleTextAttributes = textAttributes
|
edgeAppearance.titleTextAttributes = textAttributes
|
||||||
edgeAppearance.largeTitleTextAttributes = textAttributes
|
edgeAppearance.largeTitleTextAttributes = textAttributes
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
standardAppearance.backgroundColor = nil
|
standardAppearance.backgroundColor = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
self.scrollEdgeAppearance = edgeAppearance
|
self.scrollEdgeAppearance = edgeAppearance
|
||||||
self.standardAppearance = standardAppearance
|
self.standardAppearance = standardAppearance
|
||||||
} else {
|
}
|
||||||
shadowImage = UIImage()
|
else
|
||||||
|
{
|
||||||
|
self.shadowImage = UIImage()
|
||||||
|
|
||||||
if let tintColor = barTintColor {
|
if let tintColor = self.barTintColor
|
||||||
backgroundColorView.backgroundColor = tintColor
|
{
|
||||||
|
self.backgroundColorView.backgroundColor = tintColor
|
||||||
|
|
||||||
// Top = -50 to cover status bar area above navigation bar on any device.
|
// Top = -50 to cover status bar area above navigation bar on any device.
|
||||||
// Bottom = -1 to prevent a flickering gray line from appearing.
|
// Bottom = -1 to prevent a flickering gray line from appearing.
|
||||||
addSubview(backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
|
self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
|
||||||
} else {
|
}
|
||||||
barTintColor = .white
|
else
|
||||||
|
{
|
||||||
|
self.barTintColor = .white
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
if backgroundColorView.superview != nil {
|
if self.backgroundColorView.superview != nil
|
||||||
insertSubview(backgroundColorView, at: 1)
|
{
|
||||||
|
self.insertSubview(self.backgroundColorView, at: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if automaticallyAdjustsItemPositions {
|
if self.automaticallyAdjustsItemPositions
|
||||||
|
{
|
||||||
// We can't easily shift just the back button up, so we shift the entire content view slightly.
|
// We can't easily shift just the back button up, so we shift the entire content view slightly.
|
||||||
for contentView in subviews {
|
for contentView in self.subviews
|
||||||
|
{
|
||||||
guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue }
|
guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue }
|
||||||
contentView.center.y -= 2
|
contentView.center.y -= 2
|
||||||
}
|
}
|
||||||
207
AltStore/Components/PillButton.swift
Normal file
207
AltStore/Components/PillButton.swift
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
//
|
||||||
|
// PillButton.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/15/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
override var accessibilityValue: String? {
|
||||||
|
get {
|
||||||
|
guard self.progress != nil else { return super.accessibilityValue }
|
||||||
|
return self.progressView.accessibilityValue
|
||||||
|
}
|
||||||
|
set { super.accessibilityValue = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress: Progress? {
|
||||||
|
didSet {
|
||||||
|
self.progressView.progress = Float(self.progress?.fractionCompleted ?? 0)
|
||||||
|
self.progressView.observedProgress = self.progress
|
||||||
|
|
||||||
|
let isUserInteractionEnabled = self.isUserInteractionEnabled
|
||||||
|
self.isIndicatingActivity = (self.progress != nil)
|
||||||
|
self.isUserInteractionEnabled = isUserInteractionEnabled
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressTintColor: UIColor? {
|
||||||
|
get {
|
||||||
|
return self.progressView.progressTintColor
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
self.progressView.progressTintColor = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var countdownDate: Date? {
|
||||||
|
didSet {
|
||||||
|
self.isEnabled = (self.countdownDate == nil)
|
||||||
|
self.displayLink.isPaused = (self.countdownDate == nil)
|
||||||
|
|
||||||
|
if self.countdownDate == nil
|
||||||
|
{
|
||||||
|
self.setTitle(nil, for: .disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let progressView = UIProgressView(progressViewStyle: .default)
|
||||||
|
|
||||||
|
private lazy var displayLink: CADisplayLink = {
|
||||||
|
let displayLink = CADisplayLink(target: self, selector: #selector(PillButton.updateCountdown))
|
||||||
|
displayLink.preferredFramesPerSecond = 15
|
||||||
|
displayLink.isPaused = true
|
||||||
|
displayLink.add(to: .main, forMode: .common)
|
||||||
|
return displayLink
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let dateComponentsFormatter: DateComponentsFormatter = {
|
||||||
|
let dateComponentsFormatter = DateComponentsFormatter()
|
||||||
|
dateComponentsFormatter.zeroFormattingBehavior = [.pad]
|
||||||
|
dateComponentsFormatter.collapsesLargestUnit = false
|
||||||
|
return dateComponentsFormatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
let size = self.sizeThatFits(CGSize(width: Double.infinity, height: .infinity))
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit
|
||||||
|
{
|
||||||
|
self.displayLink.remove(from: .main, forMode: RunLoop.Mode.default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.layer.masksToBounds = true
|
||||||
|
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.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.progressView.progress = 0
|
||||||
|
self.progressView.trackImage = UIImage()
|
||||||
|
self.progressView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(self.progressView)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.progressView.bounds.size.width = self.bounds.width
|
||||||
|
|
||||||
|
let scale = self.bounds.height / self.progressView.bounds.height
|
||||||
|
|
||||||
|
self.progressView.transform = CGAffineTransform.identity.scaledBy(x: 1, y: scale)
|
||||||
|
self.progressView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
|
||||||
|
|
||||||
|
self.layer.cornerRadius = self.bounds.midY
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
|
super.tintColorDidChange()
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
if self.progress == nil
|
||||||
|
{
|
||||||
|
self.setTitleColor(.white, for: .normal)
|
||||||
|
self.backgroundColor = self.tintColor
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.setTitleColor(self.tintColor, for: .normal)
|
||||||
|
self.backgroundColor = self.tintColor.withAlphaComponent(0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.progressView.progressTintColor = self.tintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func updateCountdown()
|
||||||
|
{
|
||||||
|
guard let endDate = self.countdownDate else { return }
|
||||||
|
|
||||||
|
let startDate = Date()
|
||||||
|
|
||||||
|
let interval = endDate.timeIntervalSince(startDate)
|
||||||
|
guard interval > 0 else {
|
||||||
|
self.isEnabled = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let text: String?
|
||||||
|
|
||||||
|
if interval < (1 * 60 * 60)
|
||||||
|
{
|
||||||
|
self.dateComponentsFormatter.unitsStyle = .positional
|
||||||
|
self.dateComponentsFormatter.allowedUnits = [.minute, .second]
|
||||||
|
|
||||||
|
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
|
||||||
|
}
|
||||||
|
else if interval < (2 * 24 * 60 * 60)
|
||||||
|
{
|
||||||
|
self.dateComponentsFormatter.unitsStyle = .positional
|
||||||
|
self.dateComponentsFormatter.allowedUnits = [.hour, .minute, .second]
|
||||||
|
|
||||||
|
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.dateComponentsFormatter.unitsStyle = .full
|
||||||
|
self.dateComponentsFormatter.allowedUnits = [.day]
|
||||||
|
|
||||||
|
let numberOfDays = endDate.numberOfCalendarDays(since: startDate)
|
||||||
|
text = String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let text = text
|
||||||
|
{
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.isEnabled = false
|
||||||
|
self.setTitle(text, for: .disabled)
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@objc
|
class TextCollectionReusableView: UICollectionReusableView
|
||||||
public class TextCollectionReusableView: UICollectionReusableView {
|
{
|
||||||
@IBOutlet var textLabel: UILabel!
|
@IBOutlet var textLabel: UILabel!
|
||||||
|
|
||||||
@IBOutlet var topLayoutConstraint: NSLayoutConstraint!
|
@IBOutlet var topLayoutConstraint: NSLayoutConstraint!
|
||||||
147
AltStore/Components/ToastView.swift
Normal file
147
AltStore/Components/ToastView.swift
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
//
|
||||||
|
// ToastView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/19/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
extension TimeInterval
|
||||||
|
{
|
||||||
|
static let shortToastViewDuration = 4.0
|
||||||
|
static let longToastViewDuration = 8.0
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ToastView: RSTToastView
|
||||||
|
{
|
||||||
|
static let openErrorLogNotification = Notification.Name("ALTOpenErrorLogNotification")
|
||||||
|
|
||||||
|
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?)
|
||||||
|
{
|
||||||
|
if detailedText == nil
|
||||||
|
{
|
||||||
|
self.preferredDuration = .shortToastViewDuration
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.preferredDuration = .longToastViewDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init(text: text, detailText: detailedText)
|
||||||
|
|
||||||
|
self.isAccessibilityElement = true
|
||||||
|
|
||||||
|
self.layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16)
|
||||||
|
self.setNeedsLayout()
|
||||||
|
|
||||||
|
if let stackView = self.textLabel.superview as? UIStackView
|
||||||
|
{
|
||||||
|
// RSTToastView does not expose stack view containing labels,
|
||||||
|
// so we access it indirectly as the labels' superview.
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var error = error as NSError
|
||||||
|
var underlyingError = error.underlyingError
|
||||||
|
|
||||||
|
if
|
||||||
|
let unwrappedUnderlyingError = underlyingError,
|
||||||
|
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
|
||||||
|
{
|
||||||
|
// Treat underlyingError as the primary error, but keep localized title + failure.
|
||||||
|
let nsError = error 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
|
||||||
|
}
|
||||||
|
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
|
||||||
|
let detailText = error.localizedDescription
|
||||||
|
|
||||||
|
|
||||||
|
self.init(text: text, detailText: detailText)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
// Rough calculation to determine height of ToastView with one-line textLabel.
|
||||||
|
let minimumHeight = self.textLabel.font.lineHeight.rounded() + 18
|
||||||
|
self.layer.cornerRadius = minimumHeight/2
|
||||||
|
}
|
||||||
|
|
||||||
|
func show(in viewController: UIViewController)
|
||||||
|
{
|
||||||
|
self.show(in: viewController.navigationController?.view ?? viewController.view, duration: self.preferredDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
|
||||||
|
self.accessibilityLabel = announcement
|
||||||
|
|
||||||
|
// Minimum 0.75 delay to prevent announcement being cut off by VoiceOver.
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
|
||||||
|
UIAccessibility.post(notification: .announcement, argument: announcement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func show(in view: UIView)
|
||||||
|
{
|
||||||
|
self.show(in: view, duration: self.preferredDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func showErrorLog() {
|
||||||
|
guard self.opensErrorLog else { return }
|
||||||
|
NotificationCenter.default.post(name: ToastView.openErrorLogNotification, object: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
AltStore/Consts/Consts+Proxy.swift
Normal file
17
AltStore/Consts/Consts+Proxy.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// Proxy.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Joseph Mattiello on 11/7/22.
|
||||||
|
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension Consts {
|
||||||
|
enum Proxy {
|
||||||
|
static let address = "127.0.0.1"
|
||||||
|
static let port = "51820"
|
||||||
|
static let serverURL = "\(address):\(port)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,4 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Consts {}
|
public enum Consts {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,25 +7,27 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
|
||||||
#if canImport(Logging)
|
|
||||||
import Logging
|
|
||||||
#endif
|
|
||||||
|
|
||||||
extension FileManager {
|
extension FileManager
|
||||||
func directorySize(at directoryURL: URL) -> Int? {
|
{
|
||||||
|
func directorySize(at directoryURL: URL) -> Int?
|
||||||
|
{
|
||||||
guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
|
guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
|
||||||
|
|
||||||
var total = 0
|
var total: Int = 0
|
||||||
|
|
||||||
for case let fileURL as URL in enumerator {
|
for case let fileURL as URL in enumerator
|
||||||
do {
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
||||||
guard let fileSize = resourceValues.fileSize else { continue }
|
guard let fileSize = resourceValues.fileSize else { continue }
|
||||||
|
|
||||||
total += fileSize
|
total += fileSize
|
||||||
} catch {
|
}
|
||||||
os_log("Failed to read file size for item: %@. %@", type: .error, fileURL.absoluteString, error.localizedDescription)
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to read file size for item: \(fileURL).", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,8 +10,10 @@ import Intents
|
|||||||
|
|
||||||
// Requires iOS 14 in-app intent handling.
|
// Requires iOS 14 in-app intent handling.
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
extension INInteraction {
|
extension INInteraction
|
||||||
static func refreshAllApps() -> INInteraction {
|
{
|
||||||
|
static func refreshAllApps() -> INInteraction
|
||||||
|
{
|
||||||
let refreshAllIntent = RefreshAllIntent()
|
let refreshAllIntent = RefreshAllIntent()
|
||||||
refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String
|
refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ import OSLog
|
|||||||
public let customLog = OSLog(subsystem: "org.sidestore.sidestore",
|
public let customLog = OSLog(subsystem: "org.sidestore.sidestore",
|
||||||
category: "ios")
|
category: "ios")
|
||||||
|
|
||||||
|
|
||||||
public extension OSLog {
|
public extension OSLog {
|
||||||
/// Error logger extension
|
/// Error logger extension
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -48,7 +49,7 @@ public extension OSLog {
|
|||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
@inlinable
|
@inlinable
|
||||||
public func ELOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) {
|
public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||||
OSLog.error(message, args)
|
OSLog.error(message, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ public func ELOG(_ message: StaticString, file _: StaticString = #file, function
|
|||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
@inlinable
|
@inlinable
|
||||||
public func ILOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) {
|
public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||||
OSLog.info(message, args)
|
OSLog.info(message, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +67,8 @@ public func ILOG(_ message: StaticString, file _: StaticString = #file, function
|
|||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
@inlinable
|
@inlinable
|
||||||
public func DLOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) {
|
public func DLOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||||
OSLog.debug(message, args)
|
OSLog.debug(message, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Helpers
|
// mark: Helpers
|
||||||
96
AltStore/Extensions/ProcessInfo+SideStore.swift
Normal file
96
AltStore/Extensions/ProcessInfo+SideStore.swift
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
//
|
||||||
|
// ProcessInfo+SideStore.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by ny on 10/23/24.
|
||||||
|
// Copyright © 2024 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
fileprivate struct BuildVersion: Comparable {
|
||||||
|
let prefix: String
|
||||||
|
let numericPart: Int
|
||||||
|
let suffix: Character?
|
||||||
|
|
||||||
|
init?(_ buildString: String) {
|
||||||
|
// Initialize indices
|
||||||
|
var index = buildString.startIndex
|
||||||
|
|
||||||
|
// Extract prefix (letters before the numeric part)
|
||||||
|
while index < buildString.endIndex, !buildString[index].isNumber {
|
||||||
|
index = buildString.index(after: index)
|
||||||
|
}
|
||||||
|
guard index > buildString.startIndex else { return nil }
|
||||||
|
self.prefix = String(buildString[buildString.startIndex..<index])
|
||||||
|
|
||||||
|
// Extract numeric part
|
||||||
|
let startOfNumeric = index
|
||||||
|
while index < buildString.endIndex, buildString[index].isNumber {
|
||||||
|
index = buildString.index(after: index)
|
||||||
|
}
|
||||||
|
guard let numericValue = Int(buildString[startOfNumeric..<index]) else { return nil }
|
||||||
|
self.numericPart = numericValue
|
||||||
|
|
||||||
|
// Extract suffix (if any)
|
||||||
|
if index < buildString.endIndex {
|
||||||
|
self.suffix = buildString[index]
|
||||||
|
} else {
|
||||||
|
self.suffix = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Comparable protocol
|
||||||
|
static func < (lhs: BuildVersion, rhs: BuildVersion) -> Bool {
|
||||||
|
// Compare prefixes
|
||||||
|
if lhs.prefix != rhs.prefix {
|
||||||
|
return lhs.prefix < rhs.prefix
|
||||||
|
}
|
||||||
|
// Compare numeric parts
|
||||||
|
if lhs.numericPart != rhs.numericPart {
|
||||||
|
return lhs.numericPart < rhs.numericPart
|
||||||
|
}
|
||||||
|
// Compare suffixes
|
||||||
|
switch (lhs.suffix, rhs.suffix) {
|
||||||
|
case let (l?, r?):
|
||||||
|
return l < r
|
||||||
|
case (nil, _?):
|
||||||
|
return true // nil is considered less than any character
|
||||||
|
case (_?, nil):
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false // Both are nil and equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: BuildVersion, rhs: BuildVersion) -> Bool {
|
||||||
|
return lhs.prefix == rhs.prefix &&
|
||||||
|
lhs.numericPart == rhs.numericPart &&
|
||||||
|
lhs.suffix == rhs.suffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProcessInfo {
|
||||||
|
var shortVersion: String {
|
||||||
|
operatingSystemVersionString
|
||||||
|
.replacingOccurrences(of: "Version ", with: "")
|
||||||
|
.replacingOccurrences(of: "Build ", with: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var operatingSystemBuild: String {
|
||||||
|
if let start = shortVersion.range(of: "(")?.upperBound,
|
||||||
|
let end = shortVersion.range(of: ")")?.lowerBound {
|
||||||
|
shortVersion[start..<end].replacingOccurrences(of: "Build ", with: "")
|
||||||
|
} else { "???" }
|
||||||
|
}
|
||||||
|
|
||||||
|
var sparseRestorePatched: Bool {
|
||||||
|
if operatingSystemVersion < OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0) { false }
|
||||||
|
else if operatingSystemVersion > OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 1) { true }
|
||||||
|
else if operatingSystemVersion >= OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0),
|
||||||
|
let currentBuild = BuildVersion(operatingSystemBuild),
|
||||||
|
let targetBuild = BuildVersion("22B5054e") {
|
||||||
|
currentBuild >= targetBuild
|
||||||
|
} else { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,11 @@
|
|||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import ARKit
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import ARKit
|
||||||
|
|
||||||
extension UIDevice {
|
extension UIDevice
|
||||||
|
{
|
||||||
var isJailbroken: Bool {
|
var isJailbroken: Bool {
|
||||||
if
|
if
|
||||||
FileManager.default.fileExists(atPath: "/Applications/Cydia.app") ||
|
FileManager.default.fileExists(atPath: "/Applications/Cydia.app") ||
|
||||||
@@ -18,10 +19,12 @@ extension UIDevice {
|
|||||||
FileManager.default.fileExists(atPath: "/usr/sbin/sshd") ||
|
FileManager.default.fileExists(atPath: "/usr/sbin/sshd") ||
|
||||||
FileManager.default.fileExists(atPath: "/etc/apt") ||
|
FileManager.default.fileExists(atPath: "/etc/apt") ||
|
||||||
FileManager.default.fileExists(atPath: "/private/var/lib/apt/") ||
|
FileManager.default.fileExists(atPath: "/private/var/lib/apt/") ||
|
||||||
UIApplication.shared.canOpenURL(URL(string: "cydia://")!)
|
UIApplication.shared.canOpenURL(URL(string:"cydia://")!)
|
||||||
{
|
{
|
||||||
return true
|
return true
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,32 +8,37 @@
|
|||||||
|
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
import CoreHaptics
|
import CoreHaptics
|
||||||
import UIKit
|
|
||||||
|
|
||||||
private extension SystemSoundID {
|
private extension SystemSoundID
|
||||||
|
{
|
||||||
static let pop = SystemSoundID(1520)
|
static let pop = SystemSoundID(1520)
|
||||||
static let cancelled = SystemSoundID(1521)
|
static let cancelled = SystemSoundID(1521)
|
||||||
static let tryAgain = SystemSoundID(1102)
|
static let tryAgain = SystemSoundID(1102)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 13, *)
|
@available(iOS 13, *)
|
||||||
extension UIDevice {
|
extension UIDevice
|
||||||
enum VibrationPattern {
|
{
|
||||||
|
enum VibrationPattern
|
||||||
|
{
|
||||||
case success
|
case success
|
||||||
case error
|
case error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 13, *)
|
@available(iOS 13, *)
|
||||||
extension UIDevice {
|
extension UIDevice
|
||||||
|
{
|
||||||
var isVibrationSupported: Bool {
|
var isVibrationSupported: Bool {
|
||||||
CHHapticEngine.capabilitiesForHardware().supportsHaptics
|
return CHHapticEngine.capabilitiesForHardware().supportsHaptics
|
||||||
}
|
}
|
||||||
|
|
||||||
func vibrate(pattern: VibrationPattern) {
|
func vibrate(pattern: VibrationPattern)
|
||||||
guard isVibrationSupported else { return }
|
{
|
||||||
|
guard self.isVibrationSupported else { return }
|
||||||
|
|
||||||
switch pattern {
|
switch pattern
|
||||||
|
{
|
||||||
case .success: AudioServicesPlaySystemSound(.tryAgain)
|
case .success: AudioServicesPlaySystemSound(.tryAgain)
|
||||||
case .error: AudioServicesPlaySystemSound(.cancelled)
|
case .error: AudioServicesPlaySystemSound(.cancelled)
|
||||||
}
|
}
|
||||||
@@ -8,8 +8,9 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension UIScreen {
|
extension UIScreen
|
||||||
|
{
|
||||||
var isExtraCompactHeight: Bool {
|
var isExtraCompactHeight: Bool {
|
||||||
fixedCoordinateSpace.bounds.height < 600
|
return self.fixedCoordinateSpace.bounds.height < 600
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<true/>
|
<false/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
@@ -7,15 +7,13 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SideStoreCore
|
|
||||||
import Intents
|
import minimuxer
|
||||||
import OSLog
|
import AltStoreCore
|
||||||
#if canImport(Logging)
|
|
||||||
import Logging
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
public final class IntentHandler: NSObject, RefreshAllIntentHandling {
|
final class IntentHandler: NSObject, RefreshAllIntentHandling
|
||||||
|
{
|
||||||
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
|
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
|
||||||
|
|
||||||
private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]()
|
private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]()
|
||||||
@@ -23,13 +21,15 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
|
|||||||
|
|
||||||
private var operations = [RefreshAllIntent: BackgroundRefreshAppsOperation]()
|
private var operations = [RefreshAllIntent: BackgroundRefreshAppsOperation]()
|
||||||
|
|
||||||
public func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) {
|
func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
|
||||||
|
{
|
||||||
// Refreshing apps usually, but not always, completes within alotted time.
|
// Refreshing apps usually, but not always, completes within alotted time.
|
||||||
// As a workaround, we'll start refreshing apps in confirm() so we can
|
// As a workaround, we'll start refreshing apps in confirm() so we can
|
||||||
// take advantage of some extra time before starting handle() timeout timer.
|
// take advantage of some extra time before starting handle() timeout timer.
|
||||||
|
|
||||||
completionHandlers[intent] = { response in
|
self.completionHandlers[intent] = { (response) in
|
||||||
if response.code != .ready {
|
if response.code != .ready
|
||||||
|
{
|
||||||
// Operation finished before confirmation "timeout".
|
// Operation finished before confirmation "timeout".
|
||||||
// Cache response to return it when handle() is called.
|
// Cache response to return it when handle() is called.
|
||||||
self.queuedResponses[intent] = response
|
self.queuedResponses[intent] = response
|
||||||
@@ -40,39 +40,61 @@ public 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.
|
||||||
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
|
||||||
DatabaseManager.shared.start { error in
|
{
|
||||||
if let error = error {
|
DatabaseManager.shared.start() { (error) in
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription))
|
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription))
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
||||||
self.refreshApps(intent: intent)
|
self.refreshApps(intent: intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
refreshApps(intent: intent)
|
else
|
||||||
|
{
|
||||||
|
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
||||||
|
self.refreshApps(intent: intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) {
|
func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
|
||||||
completionHandlers[intent] = { response in
|
{
|
||||||
|
self.completionHandlers[intent] = { (response) in
|
||||||
// Ignore .ready response from confirm() timeout.
|
// Ignore .ready response from confirm() timeout.
|
||||||
guard response.code != .ready else { return }
|
guard response.code != .ready else { return }
|
||||||
completion(response)
|
completion(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let response = queuedResponses[intent] {
|
if let response = self.queuedResponses[intent]
|
||||||
queuedResponses[intent] = nil
|
{
|
||||||
finish(intent, response: response)
|
self.queuedResponses[intent] = nil
|
||||||
} else {
|
self.finish(intent, response: response)
|
||||||
queue.asyncAfter(deadline: .now() + 7.0) {
|
}
|
||||||
if let operation = self.operations[intent] {
|
else
|
||||||
|
{
|
||||||
|
self.queue.asyncAfter(deadline: .now() + 7.0) {
|
||||||
|
if let operation = self.operations[intent]
|
||||||
|
{
|
||||||
// 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))
|
||||||
@@ -82,36 +104,53 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
private extension IntentHandler {
|
private extension IntentHandler
|
||||||
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse) {
|
{
|
||||||
queue.async {
|
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse)
|
||||||
if let completionHandler = self.completionHandlers[intent] {
|
{
|
||||||
|
self.queue.async {
|
||||||
|
if let completionHandler = self.completionHandlers[intent]
|
||||||
|
{
|
||||||
self.completionHandlers[intent] = nil
|
self.completionHandlers[intent] = nil
|
||||||
completionHandler(response)
|
completionHandler(response)
|
||||||
} else if response.code != .ready && response.code != .inProgress {
|
}
|
||||||
|
else if response.code != .ready && response.code != .inProgress
|
||||||
|
{
|
||||||
// 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
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshApps(intent: RefreshAllIntent) {
|
func refreshApps(intent: RefreshAllIntent)
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
{
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
let installedApps = InstalledApp.fetchActiveApps(in: context)
|
let installedApps = InstalledApp.fetchActiveApps(in: context)
|
||||||
let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { result in
|
let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { (result) in
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
let results = try result.get()
|
let results = try result.get()
|
||||||
|
|
||||||
for (_, result) in results {
|
for (_, result) in results
|
||||||
|
{
|
||||||
guard case let .failure(error) = result else { continue }
|
guard case let .failure(error) = result else { continue }
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||||
} catch RefreshError.noInstalledApps {
|
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||||
|
}
|
||||||
|
catch ~RefreshErrorCode.noInstalledApps
|
||||||
|
{
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||||
} catch let error as NSError {
|
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||||
os_log("Failed to refresh apps in background. %@", type: .error , error.localizedDescription)
|
}
|
||||||
|
catch let error as NSError
|
||||||
|
{
|
||||||
|
print("Failed to refresh apps in background.", error)
|
||||||
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription))
|
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription))
|
||||||
}
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user