mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Compare commits
34 Commits
users/june
...
feature/Ro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4c2d17ffc | ||
|
|
2c829895c9 | ||
|
|
5463f2b935 | ||
|
|
d644ee7ab0 | ||
|
|
351d4fd631 | ||
|
|
128b180c1f | ||
|
|
1f2693bea6 | ||
|
|
452cf89c95 | ||
|
|
90ac0fb025 | ||
|
|
10f5ee1548 | ||
|
|
478b30c8fd | ||
|
|
207f6aac32 | ||
|
|
e1ed6f5ba3 | ||
|
|
444aac1210 | ||
|
|
f49fa24743 | ||
|
|
4c9c5b1a56 | ||
|
|
365cadbb31 | ||
|
|
36e03a52a7 | ||
|
|
19cf1722fa | ||
|
|
c28a45f100 | ||
|
|
df5b0c3af1 | ||
|
|
8b1e87d2dd | ||
|
|
e036f07875 | ||
|
|
2d232fa702 | ||
|
|
686d1ab42a | ||
|
|
d22d12c234 | ||
|
|
364b11ec9d | ||
|
|
f3a70e1e47 | ||
|
|
493b3783f0 | ||
|
|
4669227567 | ||
|
|
dfcc6e714e | ||
|
|
3b824eac96 | ||
|
|
a6559d8bb9 | ||
|
|
f270ecc537 |
21
.codecov.yml
Normal file
21
.codecov.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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,39 +1,35 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
# http://editorconfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
indent_style = space
|
||||
charset = utf-8
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,py}]
|
||||
charset = utf-8# 4 space indentation
|
||||
[*.{md,markdown}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# Swift files
|
||||
[*.swift]
|
||||
[*.{c,h,m,mm}]
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8# 4 space indentation
|
||||
indent_size = 2
|
||||
|
||||
# 4 space indentation
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
[*.{swift}]
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
# Indentation override for all JS under lib directory
|
||||
[lib/**.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
[Makefile]
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
[*.{yaml|yml}]
|
||||
indent_size = 2
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD
|
||||
* @JoeMatt @lonkelle
|
||||
|
||||
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
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
assignees: []
|
||||
assignees:
|
||||
- naturecodevoid
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
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.
|
||||
|
||||
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
- type: textarea
|
||||
id: description
|
||||
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:
|
||||
- name: Discord
|
||||
url: https://discord.gg/sidestore-949183273383395328
|
||||
url: https://discord.gg/RgpFBX3Q3k
|
||||
about: If you need support, please go here first instead of making an issue!
|
||||
- name: GitHub Discussions
|
||||
url: https://github.com/SideStore/SideStore/discussions
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -2,14 +2,15 @@ name: Feature Request
|
||||
description: Suggest a feature
|
||||
title: "[FEATURE REQUEST] "
|
||||
labels: ["enhancement"]
|
||||
assignees: []
|
||||
assignees:
|
||||
- naturecodevoid
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
|
||||
|
||||
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
||||
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
@@ -10,3 +10,6 @@
|
||||
<!-- Example: -->
|
||||
- [x] Finish UI changes
|
||||
- [ ] Test
|
||||
|
||||
<!-- If your PR doesn't close an issue, you can remove the next line. -->
|
||||
Closes #1234
|
||||
|
||||
20
.github/workflows/.disabled/sidestore-project.yml
vendored
Normal file
20
.github/workflows/.disabled/sidestore-project.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# .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,58 +20,3 @@ jobs:
|
||||
format: name
|
||||
addTo: pull
|
||||
# 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);
|
||||
}
|
||||
|
||||
139
.github/workflows/beta.yml
vendored
139
.github/workflows/beta.yml
vendored
@@ -2,7 +2,7 @@ name: Beta SideStore build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
|
||||
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -11,93 +11,80 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-14'
|
||||
version: '15.4'
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
|
||||
- name: Cache Build
|
||||
uses: irgaly/xcode-cache@v1
|
||||
with:
|
||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||
restore-keys: xcode-cache-deriveddata
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to new beta release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
|
||||
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
- name: Convert to IPA
|
||||
run: make 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
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
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
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in SideStore date form
|
||||
id: date_sidestore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to new beta release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
|
||||
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
@@ -7,7 +7,7 @@ DATE=`date -u +'%Y.%m.%d'`
|
||||
BUILD_NUM=1
|
||||
|
||||
write() {
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM/" -i '' Build.xcconfig
|
||||
echo "$DATE,$BUILD_NUM" > .nightly-build-num
|
||||
}
|
||||
|
||||
|
||||
147
.github/workflows/nightly.yml
vendored
147
.github/workflows/nightly.yml
vendored
@@ -2,7 +2,7 @@ name: Nightly SideStore build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -14,96 +14,87 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-14'
|
||||
version: '15.4'
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Cache .nightly-build-num
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .nightly-build-num
|
||||
key: nightly-build-num
|
||||
- name: Cache .nightly-build-num
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .nightly-build-num
|
||||
key: nightly-build-num
|
||||
|
||||
- name: Increase nightly build number and set as version
|
||||
run: bash .github/workflows/increase-nightly-build-num.sh
|
||||
- name: Increase nightly build number and set as version
|
||||
run: bash .github/workflows/increase-nightly-build-num.sh
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Cache Build
|
||||
uses: irgaly/xcode-cache@v1
|
||||
with:
|
||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||
restore-keys: xcode-cache-deriveddata-
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to nightly release
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release: "Nightly"
|
||||
tag: "nightly"
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
|
||||
Nightly builds are **extremely experimental builds only meant to be used by developers and alpha testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||
|
||||
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore Beta](https://github.com/${{ github.repository }}/releases?q=beta).
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
- name: Convert to IPA
|
||||
run: make 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
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
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
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in SideStore date form
|
||||
id: date_sidestore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to nightly release
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release: "Nightly"
|
||||
tag: "nightly"
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
|
||||
Nightly builds are **extremely experimental builds only meant to be used by developers and alpha testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||
|
||||
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore Beta](https://github.com/${{ github.repository }}/releases?q=beta).
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Reset cache for apps.sidestore.io/nightly
|
||||
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
|
||||
|
||||
38
.github/workflows/pr.yml
vendored
38
.github/workflows/pr.yml
vendored
@@ -9,13 +9,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-14'
|
||||
version: '15.4'
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -23,28 +23,13 @@ jobs:
|
||||
run: brew install ldid
|
||||
|
||||
- name: Add PR suffix to version
|
||||
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
|
||||
env:
|
||||
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/$/-pr.${{ github.event.pull_request.number }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
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
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
@@ -54,17 +39,14 @@ jobs:
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
name: SideStore-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
132
.github/workflows/stable.yml
vendored
132
.github/workflows/stable.yml
vendored
@@ -2,7 +2,7 @@ name: Stable SideStore build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
||||
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -11,89 +11,77 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-14'
|
||||
version: '15.4'
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Cache Build
|
||||
uses: irgaly/xcode-cache@v1
|
||||
with:
|
||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||
restore-keys: xcode-cache-deriveddata-
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to new stable release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
- name: Convert to IPA
|
||||
run: make 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
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
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
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in SideStore date form
|
||||
id: date_sidestore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to new stable release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ archive.xcarchive
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
|
||||
## Other
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
|
||||
27
.gitmodules
vendored
27
.gitmodules
vendored
@@ -1,21 +1,6 @@
|
||||
[submodule "Dependencies/Roxas"]
|
||||
path = Dependencies/Roxas
|
||||
url = https://github.com/rileytestut/Roxas.git
|
||||
[submodule "Dependencies/libimobiledevice"]
|
||||
path = Dependencies/libimobiledevice
|
||||
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
|
||||
[submodule "Dependencies/em_proxy"]
|
||||
path = SideStoreApp/Dependencies/em_proxy
|
||||
url = https://github.com/SideStore/em_proxy.git
|
||||
[submodule "Dependencies/minimuxer"]
|
||||
path = SideStoreApp/Dependencies/minimuxer
|
||||
url = https://github.com/SideStore/minimuxer.git
|
||||
|
||||
28
.jazzy.yaml
Normal file
28
.jazzy.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
# ---- 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
Normal file
42
.swiftformat
Normal file
@@ -0,0 +1,42 @@
|
||||
# .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
|
||||
76
.swiftlint.yml
Normal file
76
.swiftlint.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
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
|
||||
@@ -1,3 +0,0 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup
|
||||
@@ -1,206 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)
|
||||
@@ -1,8 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
@@ -1,570 +0,0 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// 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?()
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
//
|
||||
// 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,44 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
//
|
||||
// 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,43 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// 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)"
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
//
|
||||
// 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 }
|
||||
}
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
//
|
||||
// LaunchViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Roxas
|
||||
import EmotionalDamage
|
||||
import minimuxer
|
||||
|
||||
import AltStoreCore
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
||||
|
||||
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
||||
{
|
||||
private var didFinishLaunching = false
|
||||
|
||||
private var destinationViewController: UIViewController!
|
||||
|
||||
override var launchConditions: [RSTLaunchCondition] {
|
||||
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
|
||||
DatabaseManager.shared.start(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
return [isDatabaseStarted]
|
||||
}
|
||||
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
return self.children.first
|
||||
}
|
||||
|
||||
override var childForStatusBarHidden: UIViewController? {
|
||||
return self.children.first
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
defer {
|
||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||
}
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(true)
|
||||
if #available(iOS 17, *), !UserDefaults.standard.sidejitenable {
|
||||
DispatchQueue.global().async {
|
||||
self.isSideJITServerDetected() { result in
|
||||
DispatchQueue.main.async {
|
||||
switch result {
|
||||
case .success():
|
||||
let dialogMessage = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
|
||||
|
||||
// Create OK button with action handler
|
||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
||||
UserDefaults.standard.sidejitenable = true
|
||||
})
|
||||
|
||||
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
|
||||
//Add OK button to a dialog message
|
||||
dialogMessage.addAction(ok)
|
||||
dialogMessage.addAction(cancel)
|
||||
|
||||
// Present Alert to
|
||||
self.present(dialogMessage, animated: true, completion: nil)
|
||||
case .failure(_):
|
||||
print("Cannot find sideJITServer")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
|
||||
DispatchQueue.global().async {
|
||||
self.askfornetwork()
|
||||
}
|
||||
print("SideJITServer Enabled")
|
||||
}
|
||||
|
||||
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
|
||||
guard let pf = fetchPairingFile() else {
|
||||
displayError("Device pairing file not found.")
|
||||
return
|
||||
}
|
||||
start_minimuxer_threads(pf)
|
||||
#endif
|
||||
}
|
||||
|
||||
func askfornetwork() {
|
||||
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||
|
||||
var SJSURL = address
|
||||
|
||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||
}
|
||||
|
||||
// Create a network operation at launch to Refresh SideJITServer
|
||||
let url = URL(string: "\(SJSURL)/re/")!
|
||||
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
||||
print(data)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||
|
||||
var SJSURL = address
|
||||
|
||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||
}
|
||||
|
||||
// Create a network operation at launch to Refresh SideJITServer
|
||||
let url = URL(string: SJSURL)!
|
||||
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
||||
if let error = error {
|
||||
print("No SideJITServer on Network")
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
task.resume()
|
||||
return
|
||||
}
|
||||
|
||||
func fetchPairingFile() -> String? {
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fm = FileManager.default
|
||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
|
||||
print("Loaded ALTPairingFile from \(documentsPath.path)")
|
||||
return contents
|
||||
} else if
|
||||
let appResourcePath = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
|
||||
fm.fileExists(atPath: appResourcePath.path),
|
||||
let data = fm.contents(atPath: appResourcePath.path),
|
||||
let contents = String(data: data, encoding: .utf8),
|
||||
!contents.isEmpty,
|
||||
!UserDefaults.standard.isPairingReset {
|
||||
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
||||
return contents
|
||||
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{
|
||||
print("Loaded ALTPairingFile from Info.plist")
|
||||
return plistString
|
||||
} else {
|
||||
// Show an alert explaining the pairing file
|
||||
// Create new Alert
|
||||
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
|
||||
|
||||
// Create OK button with action handler
|
||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
||||
// Try to load it from a file picker
|
||||
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
|
||||
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
|
||||
types.append(.xml)
|
||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
||||
documentPickerController.shouldShowFileExtensions = true
|
||||
documentPickerController.delegate = self
|
||||
self.present(documentPickerController, animated: true, completion: nil)
|
||||
UserDefaults.standard.isPairingReset = false
|
||||
})
|
||||
|
||||
//Add "help" button to take user to wiki
|
||||
let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in
|
||||
let wikiURL: String = "https://docs.sidestore.io/docs/getting-started/pairing-file"
|
||||
if let url = URL(string: wikiURL) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
sleep(2)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
//Add buttons to dialog message
|
||||
dialogMessage.addAction(wikiOption)
|
||||
dialogMessage.addAction(ok)
|
||||
|
||||
// Present Alert to
|
||||
self.present(dialogMessage, animated: true, completion: nil)
|
||||
|
||||
let dialogMessage2 = UIAlertController(title: "Analytics", message: "This app contains anonymous analytics for research and project development. By continuing to use this app, you are consenting to this data collection", preferredStyle: .alert)
|
||||
|
||||
let ok2 = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in})
|
||||
|
||||
dialogMessage2.addAction(ok2)
|
||||
self.present(dialogMessage2, animated: true, completion: nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func displayError(_ msg: String) {
|
||||
print(msg)
|
||||
// Create a new alert
|
||||
let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
|
||||
|
||||
// Present alert to user
|
||||
self.present(dialogMessage, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
let url = urls[0]
|
||||
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
|
||||
|
||||
do {
|
||||
// Read to a string
|
||||
let data1 = try Data(contentsOf: urls[0])
|
||||
let pairing_string = String(bytes: data1, encoding: .utf8)
|
||||
if pairing_string == nil {
|
||||
displayError("Unable to read pairing file")
|
||||
}
|
||||
|
||||
// Save to a file for next launch
|
||||
let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")
|
||||
try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8)
|
||||
|
||||
// Start minimuxer now that we have a file
|
||||
start_minimuxer_threads(pairing_string!)
|
||||
} catch {
|
||||
displayError("Unable to read pairing file")
|
||||
}
|
||||
|
||||
if (isSecuredURL) {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
controller.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
|
||||
}
|
||||
|
||||
func start_minimuxer_threads(_ pairing_file: String) {
|
||||
target_minimuxer_address()
|
||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||
do {
|
||||
try start(pairing_file, documentsDirectory)
|
||||
} catch {
|
||||
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
||||
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
||||
}
|
||||
if #available(iOS 17, *) {
|
||||
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
|
||||
}
|
||||
else {
|
||||
start_auto_mounter(documentsDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LaunchViewController
|
||||
{
|
||||
override func handleLaunchError(_ error: Error)
|
||||
{
|
||||
do
|
||||
{
|
||||
throw error
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
||||
|
||||
let errorDescription: String
|
||||
|
||||
if #available(iOS 14.5, *)
|
||||
{
|
||||
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
|
||||
errorDescription = errorMessages.joined(separator: "\n\n")
|
||||
}
|
||||
else
|
||||
{
|
||||
errorDescription = error.debugDescription
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
||||
self.handleLaunchConditions()
|
||||
}))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func finishLaunching()
|
||||
{
|
||||
super.finishLaunching()
|
||||
|
||||
guard !self.didFinishLaunching else { return }
|
||||
|
||||
AppManager.shared.update()
|
||||
AppManager.shared.updatePatronsIfNeeded()
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
|
||||
// Add view controller as child (rather than presenting modally)
|
||||
// so tint adjustment + card presentations works correctly.
|
||||
self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
|
||||
self.destinationViewController.view.alpha = 0.0
|
||||
self.addChild(self.destinationViewController)
|
||||
self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
|
||||
self.destinationViewController.didMove(toParent: self)
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.destinationViewController.view.alpha = 1.0
|
||||
}
|
||||
|
||||
self.didFinishLaunching = true
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// AppExtensionView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by June P on 8/17/24.
|
||||
// Copyright © 2024 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CAltSign
|
||||
|
||||
extension ALTApplication: Identifiable {}
|
||||
|
||||
struct AppExtensionView: View {
|
||||
var extensions: Set<ALTApplication>
|
||||
@State var selection: [ALTApplication] = []
|
||||
|
||||
var completion: (_ selection: [ALTApplication]) -> Any?
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(self.extensions.sorted {
|
||||
$0.bundleIdentifier < $1.bundleIdentifier
|
||||
}, id: \.self) { item in
|
||||
MultipleSelectionRow(title: item.bundleIdentifier, isSelected: !selection.contains(item)) {
|
||||
if self.selection.contains(item) {
|
||||
self.selection.removeAll(where: { $0 == item })
|
||||
}
|
||||
else {
|
||||
self.selection.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("App Extensions")
|
||||
.onDisappear {
|
||||
_ = completion(selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MultipleSelectionRow: View {
|
||||
var title: String
|
||||
var isSelected: Bool
|
||||
var action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
SwiftUI.Button(action: self.action) {
|
||||
HStack {
|
||||
Text(self.title)
|
||||
if self.isSelected {
|
||||
Spacer()
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppExtensionViewHostingController: UIHostingController<AppExtensionView> {
|
||||
|
||||
|
||||
var completion: Optional<(_ selection: [ALTApplication]) -> Any?> = nil
|
||||
|
||||
required init(extensions: Set<ALTApplication>, completion: @escaping (_ selection: [ALTApplication]) -> Any?) {
|
||||
self.completion = completion
|
||||
super.init(rootView: AppExtensionView(extensions: extensions, completion: completion))
|
||||
}
|
||||
|
||||
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppExtensionViewHostingController: UIPopoverPresentationControllerDelegate {
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
//
|
||||
// AppManagerErrors.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/27/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
struct FetchSourcesError: LocalizedError, CustomNSError
|
||||
{
|
||||
var primaryError: Error?
|
||||
|
||||
var sources: Set<Source>?
|
||||
var errors = [Source: Error]()
|
||||
|
||||
var managedObjectContext: NSManagedObjectContext?
|
||||
|
||||
var localizedTitle: String? {
|
||||
var localizedTitle: String?
|
||||
self.managedObjectContext?.performAndWait {
|
||||
if self.sources?.count == 1 {
|
||||
localizedTitle = NSLocalizedString("Failed to refresh Store", comment: "")
|
||||
} else if self.errors.count == 1 {
|
||||
guard let source = self.errors.keys.first else { return }
|
||||
localizedTitle = String(format: NSLocalizedString("Failed to refresh Source '%@'", comment: ""), source.name)
|
||||
} else {
|
||||
localizedTitle = String(format: NSLocalizedString("Failed to refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count))
|
||||
}
|
||||
}
|
||||
return localizedTitle
|
||||
}
|
||||
|
||||
var errorDescription: String? {
|
||||
if let error = self.primaryError {
|
||||
return error.localizedDescription
|
||||
} else if let error = self.errors.values.first, self.errors.count == 1 {
|
||||
return error.localizedDescription
|
||||
} else {
|
||||
var localizedDescription: String?
|
||||
|
||||
self.managedObjectContext?.performAndWait {
|
||||
if self.sources?.count == 1
|
||||
{
|
||||
localizedDescription = NSLocalizedString("Could not refresh store.", comment: "")
|
||||
}
|
||||
else if self.errors.count == 1
|
||||
{
|
||||
guard let source = self.errors.keys.first else { return }
|
||||
localizedDescription = String(format: NSLocalizedString("Could not refresh source “%@”.", comment: ""), source.name)
|
||||
}
|
||||
else
|
||||
{
|
||||
localizedDescription = String(format: NSLocalizedString("Could not refresh %@ sources.", comment: ""), NSNumber(value: self.errors.count))
|
||||
}
|
||||
}
|
||||
|
||||
return localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
if let error = self.primaryError as NSError?
|
||||
{
|
||||
return error.localizedRecoverySuggestion
|
||||
}
|
||||
else if self.errors.count == 1
|
||||
{
|
||||
return nil
|
||||
}
|
||||
else
|
||||
{
|
||||
return NSLocalizedString("Tap to view source errors.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var errorUserInfo: [String : Any] {
|
||||
let errors = Array(self.errors.values)
|
||||
var userInfo = [String: Any]()
|
||||
userInfo[ALTLocalizedTitleErrorKey] = self.localizedTitle
|
||||
userInfo[NSUnderlyingErrorKey] = self.primaryError
|
||||
if #available(iOS 14.5, *), !errors.isEmpty {
|
||||
userInfo[NSMultipleUnderlyingErrorsKey] = errors
|
||||
}
|
||||
return userInfo
|
||||
}
|
||||
|
||||
init(_ error: Error)
|
||||
{
|
||||
self.primaryError = error
|
||||
}
|
||||
|
||||
init(sources: Set<Source>, errors: [Source: Error], context: NSManagedObjectContext)
|
||||
{
|
||||
self.sources = sources
|
||||
self.errors = errors
|
||||
self.managedObjectContext = context
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// InstalledAppsCollectionHeaderView.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 3/9/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
||||
{
|
||||
let textLabel: UILabel
|
||||
let button: UIButton
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
self.textLabel = UILabel()
|
||||
self.textLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.textLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
|
||||
self.textLabel.accessibilityTraits.insert(.header)
|
||||
|
||||
self.button = UIButton(type: .system)
|
||||
self.button.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.textLabel)
|
||||
self.addSubview(self.button)
|
||||
|
||||
NSLayoutConstraint.activate([self.textLabel.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
|
||||
self.textLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor)])
|
||||
|
||||
NSLayoutConstraint.activate([self.button.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor),
|
||||
self.button.firstBaselineAnchor.constraint(equalTo: self.textLabel.firstBaselineAnchor)])
|
||||
|
||||
self.preservesSuperviewLayoutMargins = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
//
|
||||
// UpdateCollectionViewCell.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/16/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UpdateCollectionViewCell
|
||||
{
|
||||
enum Mode
|
||||
{
|
||||
case collapsed
|
||||
case expanded
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class UpdateCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
var mode: Mode = .expanded {
|
||||
didSet {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet var bannerView: AppBannerView!
|
||||
@IBOutlet var versionDescriptionTitleLabel: UILabel!
|
||||
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
||||
|
||||
@IBOutlet private var blurView: UIVisualEffectView!
|
||||
|
||||
private var originalTintColor: UIColor?
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
// Prevent temporary unsatisfiable constraint errors due to UIView-Encapsulated-Layout constraints.
|
||||
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.bannerView.backgroundEffectView.isHidden = true
|
||||
self.bannerView.button.setTitle(NSLocalizedString("UPDATE", comment: ""), for: .normal)
|
||||
|
||||
self.blurView.layer.cornerRadius = 20
|
||||
self.blurView.layer.masksToBounds = true
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func tintColorDidChange()
|
||||
{
|
||||
super.tintColorDidChange()
|
||||
|
||||
if self.tintAdjustmentMode != .dimmed
|
||||
{
|
||||
self.originalTintColor = self.tintColor
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes)
|
||||
{
|
||||
// Animates transition to new attributes.
|
||||
let animator = UIViewPropertyAnimator(springTimingParameters: UISpringTimingParameters()) {
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
animator.startAnimation()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
|
||||
{
|
||||
let view = super.hitTest(point, with: event)
|
||||
|
||||
if view == self.versionDescriptionTextView
|
||||
{
|
||||
// Forward touches on the text view (but not on the nested "more" button)
|
||||
// so cell selection works as expected.
|
||||
return self
|
||||
}
|
||||
else
|
||||
{
|
||||
return view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension UpdateCollectionViewCell
|
||||
{
|
||||
func update()
|
||||
{
|
||||
switch self.mode
|
||||
{
|
||||
case .collapsed: self.versionDescriptionTextView.isCollapsed = true
|
||||
case .expanded: self.versionDescriptionTextView.isCollapsed = false
|
||||
}
|
||||
|
||||
self.versionDescriptionTitleLabel.textColor = self.originalTintColor ?? self.tintColor
|
||||
self.blurView.backgroundColor = self.originalTintColor ?? self.tintColor
|
||||
self.bannerView.button.progressTintColor = self.originalTintColor ?? self.tintColor
|
||||
|
||||
self.setNeedsLayout()
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// NewsCollectionViewCell.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/29/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class NewsCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
@IBOutlet var titleLabel: UILabel!
|
||||
@IBOutlet var captionLabel: UILabel!
|
||||
@IBOutlet var imageView: UIImageView!
|
||||
@IBOutlet var contentBackgroundView: UIView!
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.contentBackgroundView.layer.cornerRadius = 30
|
||||
self.contentBackgroundView.clipsToBounds = true
|
||||
|
||||
self.imageView.layer.cornerRadius = 30
|
||||
self.imageView.clipsToBounds = true
|
||||
}
|
||||
}
|
||||
@@ -1,529 +0,0 @@
|
||||
//
|
||||
// NewsViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/29/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
private final class AppBannerFooterView: UICollectionReusableView
|
||||
{
|
||||
let bannerView = AppBannerView(frame: .zero)
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addGestureRecognizer(self.tapGestureRecognizer)
|
||||
|
||||
self.bannerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.addSubview(self.bannerView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
self.bannerView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
self.bannerView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
self.bannerView.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
|
||||
self.bannerView.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
final class NewsViewController: UICollectionViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
||||
|
||||
private var prototypeCell: NewsCollectionViewCell!
|
||||
|
||||
private var loadingState: LoadingState = .loading {
|
||||
didSet {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
// Cache
|
||||
private var cachedCellSizes = [String: CGSize]()
|
||||
|
||||
required init?(coder: NSCoder)
|
||||
{
|
||||
super.init(coder: coder)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(NewsViewController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.prototypeCell = NewsCollectionViewCell.instantiate(with: NewsCollectionViewCell.nib!)
|
||||
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
self.collectionView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.collectionView.register(NewsCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
self.collectionView.register(AppBannerFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner")
|
||||
|
||||
self.registerForPreviewing(with: self, sourceView: self.collectionView)
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.fetchSource()
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews()
|
||||
{
|
||||
super.viewWillLayoutSubviews()
|
||||
|
||||
if self.collectionView.contentInset.bottom != 20
|
||||
{
|
||||
// Triggers collection view update in iOS 13, which crashes if we do it in viewDidLoad()
|
||||
// since the database might not be loaded yet.
|
||||
self.collectionView.contentInset.bottom = 20
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
|
||||
{
|
||||
self.fetchSource()
|
||||
}
|
||||
}
|
||||
|
||||
private extension NewsViewController
|
||||
{
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>
|
||||
{
|
||||
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
|
||||
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
|
||||
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)]
|
||||
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.objectID), cacheName: nil)
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||
dataSource.proxy = self
|
||||
dataSource.cellConfigurationHandler = { (cell, newsItem, indexPath) in
|
||||
let cell = cell as! NewsCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.titleLabel.text = newsItem.title
|
||||
cell.captionLabel.text = newsItem.caption
|
||||
cell.contentBackgroundView.backgroundColor = newsItem.tintColor
|
||||
|
||||
cell.imageView.image = nil
|
||||
|
||||
if newsItem.imageURL != nil
|
||||
{
|
||||
cell.imageView.isIndicatingActivity = true
|
||||
cell.imageView.isHidden = false
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.imageView.isIndicatingActivity = false
|
||||
cell.imageView.isHidden = true
|
||||
}
|
||||
|
||||
cell.isAccessibilityElement = true
|
||||
cell.accessibilityLabel = (cell.titleLabel.text ?? "") + ". " + (cell.captionLabel.text ?? "")
|
||||
|
||||
if newsItem.storeApp != nil || newsItem.externalURL != nil
|
||||
{
|
||||
cell.accessibilityTraits.insert(.button)
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.accessibilityTraits.remove(.button)
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (newsItem, indexPath, completionHandler) in
|
||||
guard let imageURL = newsItem.imageURL else { return nil }
|
||||
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: imageURL, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completionHandler(image, nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! NewsCollectionViewCell
|
||||
cell.imageView.isIndicatingActivity = false
|
||||
cell.imageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
dataSource.placeholderView = self.placeholderView
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func fetchSource()
|
||||
{
|
||||
self.loadingState = .loading
|
||||
|
||||
AppManager.shared.fetchSources() { (result) in
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
let (_, context) = try result.get()
|
||||
try context.save()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.loadingState = .finished(.success(()))
|
||||
}
|
||||
}
|
||||
catch let error as AppManager.FetchSourcesError
|
||||
{
|
||||
try error.managedObjectContext?.save()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
if self.dataSource.itemCount > 0
|
||||
{
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
|
||||
self.loadingState = .finished(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
switch self.loadingState
|
||||
{
|
||||
case .loading:
|
||||
self.placeholderView.textLabel.isHidden = true
|
||||
self.placeholderView.detailTextLabel.isHidden = false
|
||||
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
|
||||
|
||||
self.placeholderView.activityIndicatorView.startAnimating()
|
||||
|
||||
case .finished(.failure(let error)):
|
||||
self.placeholderView.textLabel.isHidden = false
|
||||
self.placeholderView.detailTextLabel.isHidden = false
|
||||
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch News", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = error.localizedDescription
|
||||
|
||||
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||
|
||||
case .finished(.success):
|
||||
self.placeholderView.textLabel.isHidden = true
|
||||
self.placeholderView.detailTextLabel.isHidden = true
|
||||
|
||||
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension NewsViewController
|
||||
{
|
||||
@objc func handleTapGesture(_ gestureRecognizer: UITapGestureRecognizer)
|
||||
{
|
||||
guard let footerView = gestureRecognizer.view as? UICollectionReusableView else { return }
|
||||
|
||||
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
|
||||
|
||||
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
|
||||
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||
return supplementaryView == footerView
|
||||
}) else { return }
|
||||
|
||||
let item = self.dataSource.item(at: indexPath)
|
||||
guard let storeApp = item.storeApp else { return }
|
||||
|
||||
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||
self.navigationController?.pushViewController(appViewController, animated: true)
|
||||
}
|
||||
|
||||
@objc func performAppAction(_ sender: PillButton)
|
||||
{
|
||||
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
|
||||
|
||||
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
|
||||
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||
return supplementaryView?.frame.contains(point) ?? false
|
||||
}) else { return }
|
||||
|
||||
let app = self.dataSource.item(at: indexPath)
|
||||
guard let storeApp = app.storeApp else { return }
|
||||
|
||||
if let installedApp = app.storeApp?.installedApp
|
||||
{
|
||||
self.open(installedApp)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.install(storeApp, at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func install(_ storeApp: StoreApp, at indexPath: IndexPath)
|
||||
{
|
||||
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
|
||||
guard previousProgress == nil else {
|
||||
previousProgress?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
_ = AppManager.shared.install(storeApp, presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(OperationError.cancelled): break // Ignore
|
||||
case .failure(let error):
|
||||
ToastView(error: error, opensLog: true).show(in: self)
|
||||
|
||||
case .success: print("Installed app:", storeApp.bundleIdentifier)
|
||||
}
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
|
||||
}
|
||||
}
|
||||
|
||||
func open(_ installedApp: InstalledApp)
|
||||
{
|
||||
UIApplication.shared.open(installedApp.openAppURL)
|
||||
}
|
||||
}
|
||||
|
||||
private extension NewsViewController
|
||||
{
|
||||
@objc func importApp(_ notification: Notification)
|
||||
{
|
||||
self.presentedViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsViewController
|
||||
{
|
||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||
{
|
||||
let newsItem = self.dataSource.item(at: indexPath)
|
||||
|
||||
if let externalURL = newsItem.externalURL
|
||||
{
|
||||
let safariViewController = SFSafariViewController(url: externalURL)
|
||||
safariViewController.preferredControlTintColor = newsItem.tintColor
|
||||
self.present(safariViewController, animated: true, completion: nil)
|
||||
}
|
||||
else if let storeApp = newsItem.storeApp
|
||||
{
|
||||
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||
self.navigationController?.pushViewController(appViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||
{
|
||||
let item = self.dataSource.item(at: indexPath)
|
||||
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner", for: indexPath) as! AppBannerFooterView
|
||||
guard let storeApp = item.storeApp else { return footerView }
|
||||
|
||||
footerView.layoutMargins.left = self.view.layoutMargins.left
|
||||
footerView.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
footerView.bannerView.configure(for: storeApp)
|
||||
|
||||
footerView.bannerView.tintColor = storeApp.tintColor
|
||||
footerView.bannerView.button.addTarget(self, action: #selector(NewsViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
footerView.tapGestureRecognizer.addTarget(self, action: #selector(NewsViewController.handleTapGesture(_:)))
|
||||
|
||||
footerView.bannerView.button.isIndicatingActivity = false
|
||||
|
||||
if storeApp.installedApp == nil
|
||||
{
|
||||
let buttonTitle = NSLocalizedString("Free", comment: "")
|
||||
footerView.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
||||
footerView.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), storeApp.name)
|
||||
footerView.bannerView.button.accessibilityValue = buttonTitle
|
||||
|
||||
let progress = AppManager.shared.installationProgress(for: storeApp)
|
||||
footerView.bannerView.button.progress = progress
|
||||
|
||||
if let versionDate = storeApp.latestSupportedVersion?.date, versionDate > Date()
|
||||
{
|
||||
footerView.bannerView.button.countdownDate = versionDate
|
||||
}
|
||||
else
|
||||
{
|
||||
footerView.bannerView.button.countdownDate = nil
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
footerView.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||
footerView.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), storeApp.name)
|
||||
footerView.bannerView.button.accessibilityValue = nil
|
||||
footerView.bannerView.button.progress = nil
|
||||
footerView.bannerView.button.countdownDate = nil
|
||||
}
|
||||
|
||||
Nuke.loadImage(with: storeApp.iconURL, into: footerView.bannerView.iconImageView)
|
||||
|
||||
return footerView
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsViewController: UICollectionViewDelegateFlowLayout
|
||||
{
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||
{
|
||||
let item = self.dataSource.item(at: indexPath)
|
||||
|
||||
if let previousSize = self.cachedCellSizes[item.identifier]
|
||||
{
|
||||
return previousSize
|
||||
}
|
||||
|
||||
// Take layout margins into account.
|
||||
self.prototypeCell.layoutMargins.left = self.view.layoutMargins.left
|
||||
self.prototypeCell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||
NSLayoutConstraint.activate([widthConstraint])
|
||||
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
|
||||
|
||||
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
|
||||
|
||||
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
self.cachedCellSizes[item.identifier] = size
|
||||
return size
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||
{
|
||||
let item = self.dataSource.item(at: IndexPath(row: 0, section: section))
|
||||
|
||||
if item.storeApp != nil
|
||||
{
|
||||
return CGSize(width: 88, height: 88)
|
||||
}
|
||||
else
|
||||
{
|
||||
return .zero
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
|
||||
{
|
||||
var insets = UIEdgeInsets(top: 30, left: 0, bottom: 13, right: 0)
|
||||
|
||||
if section == 0
|
||||
{
|
||||
insets.top = 10
|
||||
}
|
||||
|
||||
return insets
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsViewController: UIViewControllerPreviewingDelegate
|
||||
{
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
|
||||
{
|
||||
if let indexPath = self.collectionView.indexPathForItem(at: location), let cell = self.collectionView.cellForItem(at: indexPath)
|
||||
{
|
||||
// Previewing news item.
|
||||
|
||||
previewingContext.sourceRect = cell.frame
|
||||
|
||||
let newsItem = self.dataSource.item(at: indexPath)
|
||||
|
||||
if let externalURL = newsItem.externalURL
|
||||
{
|
||||
let safariViewController = SFSafariViewController(url: externalURL)
|
||||
safariViewController.preferredControlTintColor = newsItem.tintColor
|
||||
return safariViewController
|
||||
}
|
||||
else if let storeApp = newsItem.storeApp
|
||||
{
|
||||
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||
return appViewController
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
else
|
||||
{
|
||||
// Previewing app banner (or nothing).
|
||||
|
||||
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
|
||||
|
||||
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
|
||||
let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||
return layoutAttributes?.frame.contains(location) ?? false
|
||||
}) else { return nil }
|
||||
|
||||
guard let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath) else { return nil }
|
||||
previewingContext.sourceRect = layoutAttributes.frame
|
||||
|
||||
let item = self.dataSource.item(at: indexPath)
|
||||
guard let storeApp = item.storeApp else { return nil }
|
||||
|
||||
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||
return appViewController
|
||||
}
|
||||
}
|
||||
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
|
||||
{
|
||||
if let safariViewController = viewControllerToCommit as? SFSafariViewController
|
||||
{
|
||||
self.present(safariViewController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
//
|
||||
// ClearAppCacheOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/27/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltStoreCore
|
||||
/*
|
||||
struct BatchError: ALTLocalizedError
|
||||
{
|
||||
|
||||
enum Code: Int, ALTErrorCode
|
||||
{
|
||||
typealias Error = BatchError
|
||||
|
||||
case batchError
|
||||
}
|
||||
|
||||
var code: Code = .batchError
|
||||
var underlyingErrors: [Error]
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
init(errors: [Error])
|
||||
{
|
||||
self.underlyingErrors = errors
|
||||
}
|
||||
|
||||
var errorFailureReason: String {
|
||||
guard !self.underlyingErrors.isEmpty else { return NSLocalizedString("An unknown error occured.", comment: "") }
|
||||
|
||||
let errorMessages = self.underlyingErrors.map { $0.localizedDescription }
|
||||
|
||||
let message = errorMessages.joined(separator: "\n\n")
|
||||
return message
|
||||
}
|
||||
}
|
||||
*/
|
||||
@objc(ClearAppCacheOperation)
|
||||
class ClearAppCacheOperation: ResultOperation<Void>
|
||||
{
|
||||
private let coordinator = NSFileCoordinator()
|
||||
private let coordinatorQueue = OperationQueue()
|
||||
|
||||
override init()
|
||||
{
|
||||
self.coordinatorQueue.name = "AltStore - ClearAppCacheOperation Queue"
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
var allErrors = [Error]()
|
||||
|
||||
self.clearTemporaryDirectory { result in
|
||||
switch result
|
||||
{
|
||||
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
|
||||
case .failure(let error): allErrors.append(error)
|
||||
case .success: break
|
||||
}
|
||||
|
||||
self.removeUninstalledAppBackupDirectories { result in
|
||||
switch result
|
||||
{
|
||||
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
|
||||
case .failure(let error): allErrors.append(error)
|
||||
case .success: break
|
||||
}
|
||||
|
||||
if allErrors.isEmpty
|
||||
{
|
||||
self.finish(.success(()))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.finish(.failure(OperationError.cacheClearError(errors: allErrors.map({ error in
|
||||
return error.localizedDescription
|
||||
}))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ClearAppCacheOperation
|
||||
{
|
||||
func clearTemporaryDirectory(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.writingIntent(with: FileManager.default.temporaryDirectory, options: [.forDeleting])
|
||||
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
|
||||
includingPropertiesForKeys: [],
|
||||
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
|
||||
var errors = [Error]()
|
||||
|
||||
for fileURL in fileURLs
|
||||
{
|
||||
do
|
||||
{
|
||||
print("[ALTLog] Removing item from temporary directory:", fileURL.lastPathComponent)
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("[ALTLog] Failed to remove \(fileURL.lastPathComponent) from temporary directory.", error)
|
||||
errors.append(error)
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.isEmpty
|
||||
{
|
||||
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
|
||||
return error.localizedDescription
|
||||
}))))
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeUninstalledAppBackupDirectories(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
guard let backupsDirectory = FileManager.default.appBackupsDirectory else { return completion(.failure(OperationError.missingAppGroup)) }
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||
let installedAppBundleIDs = Set(InstalledApp.all(in: context).map { $0.bundleIdentifier })
|
||||
|
||||
let intent = NSFileAccessIntent.writingIntent(with: backupsDirectory, options: [.forDeleting])
|
||||
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: intent.url.path, isDirectory: &isDirectory), isDirectory.boolValue else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
|
||||
includingPropertiesForKeys: [.isDirectoryKey, .nameKey],
|
||||
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
|
||||
var errors = [Error]()
|
||||
|
||||
|
||||
for backupDirectory in fileURLs
|
||||
{
|
||||
do
|
||||
{
|
||||
let resourceValues = try backupDirectory.resourceValues(forKeys: [.isDirectoryKey, .nameKey])
|
||||
guard let isDirectory = resourceValues.isDirectory, let bundleID = resourceValues.name else { continue }
|
||||
|
||||
if isDirectory && !installedAppBundleIDs.contains(bundleID) && !AppManager.shared.isActivelyManagingApp(withBundleID: bundleID)
|
||||
{
|
||||
print("[ALTLog] Removing backup directory for uninstalled app:", bundleID)
|
||||
try FileManager.default.removeItem(at: backupDirectory)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("[ALTLog] Failed to remove app backup directory:", error)
|
||||
errors.append(error)
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.isEmpty
|
||||
{
|
||||
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
|
||||
return error.localizedDescription
|
||||
}))))
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("[ALTLog] Failed to remove app backup directory:", error)
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
//
|
||||
// DownloadAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/10/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
@objc(DownloadAppOperation)
|
||||
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let app: AppProtocol
|
||||
let context: AppOperationContext
|
||||
|
||||
private let appName: String
|
||||
private let bundleIdentifier: String
|
||||
private var sourceURL: URL?
|
||||
private let destinationURL: URL
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
||||
{
|
||||
self.app = app
|
||||
self.context = context
|
||||
|
||||
self.appName = app.name
|
||||
self.bundleIdentifier = app.bundleIdentifier
|
||||
self.sourceURL = app.url
|
||||
self.destinationURL = destinationURL
|
||||
|
||||
super.init()
|
||||
|
||||
// App = 3, Dependencies = 1
|
||||
self.progress.totalUnitCount = 4
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
print("Downloading App:", self.bundleIdentifier)
|
||||
|
||||
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
|
||||
|
||||
guard let storeApp = self.app as? StoreApp else { return self.download(self.app) }
|
||||
storeApp.managedObjectContext?.perform {
|
||||
do {
|
||||
let latestVersion = try self.verify(storeApp)
|
||||
self.download(latestVersion)
|
||||
} catch let error as VerificationError where error.code == .iOSVersionNotSupported {
|
||||
guard let presentingViewController = self.context.presentingViewController,
|
||||
let latestSupportedVersion = storeApp.latestSupportedVersion,
|
||||
case let version = latestSupportedVersion.version,
|
||||
version != storeApp.installedApp?.version else {
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
|
||||
let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("Download %@ %@", comment: ""), self.appName, version), style: .default) { _ in
|
||||
self.download(latestSupportedVersion)
|
||||
})
|
||||
presentingViewController.present(alertController, animated: true)
|
||||
}
|
||||
} catch {
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<ALTApplication, any Error>) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
||||
} catch {
|
||||
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
||||
}
|
||||
super.finish(result)
|
||||
}
|
||||
}
|
||||
|
||||
private extension DownloadAppOperation {
|
||||
func verify(_ storeApp: StoreApp) throws -> AppVersion {
|
||||
guard let version = storeApp.latestAvailableVersion else {
|
||||
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
|
||||
throw OperationError.unknown(failureReason: failureReason)
|
||||
}
|
||||
if let minOSVersion = version.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
|
||||
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: minOSVersion)
|
||||
} else if let maxOSVersion = version.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
|
||||
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: maxOSVersion)
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
func download(@Managed _ app: AppProtocol) {
|
||||
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
|
||||
|
||||
self.downloadIPA(from: sourceURL) { result in
|
||||
do
|
||||
{
|
||||
let application = try result.get()
|
||||
|
||||
if self.context.bundleIdentifier == StoreApp.dolphinAppID, self.context.bundleIdentifier != application.bundleIdentifier
|
||||
{
|
||||
if var infoPlist = NSDictionary(contentsOf: application.bundle.infoPlistURL) as? [String: Any]
|
||||
{
|
||||
// Manually update the app's bundle identifier to match the one specified in the source.
|
||||
// This allows people who previously installed the app to still update and refresh normally.
|
||||
infoPlist[kCFBundleIdentifierKey as String] = StoreApp.dolphinAppID
|
||||
(infoPlist as NSDictionary).write(to: application.bundle.infoPlistURL, atomically: true)
|
||||
}
|
||||
}
|
||||
|
||||
self.downloadDependencies(for: application) { result in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
|
||||
try FileManager.default.copyItem(at: application.fileURL, to: self.destinationURL, shouldReplace: true)
|
||||
|
||||
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
|
||||
self.finish(.success(copiedApplication))
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||
{
|
||||
func finishOperation(_ result: Result<URL, Error>)
|
||||
{
|
||||
do
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
|
||||
|
||||
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL: URL
|
||||
|
||||
if isDirectory.boolValue
|
||||
{
|
||||
// Directory, so assuming this is .app bundle.
|
||||
guard Bundle(url: fileURL) != nil else { throw OperationError.invalidApp }
|
||||
|
||||
appBundleURL = self.temporaryDirectory.appendingPathComponent(fileURL.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
// File, so assuming this is a .ipa file.
|
||||
appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: self.temporaryDirectory)
|
||||
}
|
||||
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
completionHandler(.success(application))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
if sourceURL.isFileURL
|
||||
{
|
||||
finishOperation(.success(sourceURL))
|
||||
|
||||
self.progress.completedUnitCount += 3
|
||||
}
|
||||
else
|
||||
{
|
||||
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
if let response = response as? HTTPURLResponse {
|
||||
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: sourceURL]) }
|
||||
}
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
finishOperation(.success(fileURL))
|
||||
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
finishOperation(.failure(error))
|
||||
}
|
||||
}
|
||||
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 3)
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension DownloadAppOperation
|
||||
{
|
||||
struct AltStorePlist: Decodable
|
||||
{
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case dependencies = "ALTDependencies"
|
||||
}
|
||||
|
||||
var dependencies: [Dependency]
|
||||
}
|
||||
|
||||
struct Dependency: Decodable
|
||||
{
|
||||
var downloadURL: URL
|
||||
var path: String?
|
||||
|
||||
var preferredFilename: String {
|
||||
let preferredFilename = self.path.map { ($0 as NSString).lastPathComponent } ?? self.downloadURL.lastPathComponent
|
||||
return preferredFilename
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case downloadURL
|
||||
case path
|
||||
}
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let urlString = try container.decode(String.self, forKey: .downloadURL)
|
||||
let path = try container.decodeIfPresent(String.self, forKey: .path)
|
||||
|
||||
guard let downloadURL = URL(string: urlString) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "downloadURL is not a valid URL.")
|
||||
}
|
||||
|
||||
self.downloadURL = downloadURL
|
||||
self.path = path
|
||||
}
|
||||
}
|
||||
|
||||
func downloadDependencies(for application: ALTApplication, completionHandler: @escaping (Result<Set<URL>, Error>) -> Void)
|
||||
{
|
||||
guard FileManager.default.fileExists(atPath: application.bundle.altstorePlistURL.path) else {
|
||||
return completionHandler(.success([]))
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
let data = try Data(contentsOf: application.bundle.altstorePlistURL)
|
||||
|
||||
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
|
||||
|
||||
var dependencyURLs = Set<URL>()
|
||||
var dependencyError: Error?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
|
||||
|
||||
for dependency in altstorePlist.dependencies
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.download(dependency, for: application, progress: progress) { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): dependencyError = error
|
||||
case .success(let fileURL): dependencyURLs.insert(fileURL)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(qos: .userInitiated, queue: .global()) {
|
||||
if let dependencyError = dependencyError
|
||||
{
|
||||
completionHandler(.failure(dependencyError))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(dependencyURLs))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch let error as DecodingError
|
||||
{
|
||||
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not determine dependencies for %@.", comment: ""), application.name))
|
||||
completionHandler(.failure(nsError))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
|
||||
let path = dependency.path ?? dependency.preferredFilename
|
||||
let destinationURL = application.fileURL.appendingPathComponent(path)
|
||||
|
||||
let directoryURL = destinationURL.deletingLastPathComponent()
|
||||
if !FileManager.default.fileExists(atPath: directoryURL.path)
|
||||
{
|
||||
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
completionHandler(.success(destinationURL))
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
let localizedFailure = String(format: NSLocalizedString("The dependency '%@' could not be downloaded.", comment: ""), dependency.preferredFilename)
|
||||
completionHandler(.failure(error.withLocalizedFailure(localizedFailure)))
|
||||
}
|
||||
}
|
||||
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
//
|
||||
// EnableJITOperation.swift
|
||||
// EnableJITOperation
|
||||
//
|
||||
// Created by Riley Testut on 9/1/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import minimuxer
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
enum SideJITServerErrorType: Error {
|
||||
case invalidURL
|
||||
case errorConnecting
|
||||
case deviceNotFound
|
||||
case other(String)
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
protocol EnableJITContext
|
||||
{
|
||||
var installedApp: InstalledApp? { get }
|
||||
|
||||
var error: Error? { get }
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
||||
{
|
||||
let context: Context
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
init(context: Context)
|
||||
{
|
||||
self.context = context
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let installedApp = self.context.installedApp else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil")))
|
||||
}
|
||||
if #available(iOS 17, *) {
|
||||
let sideJITenabled = UserDefaults.standard.sidejitenable
|
||||
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||
|
||||
if sideJITenabled {
|
||||
installedApp.managedObjectContext?.perform {
|
||||
EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case .invalidURL, .errorConnecting:
|
||||
self.finish(.failure(OperationError.unableToConnectSideJIT))
|
||||
case .deviceNotFound:
|
||||
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
|
||||
case .other(let message):
|
||||
if let startRange = message.range(of: "<p>"),
|
||||
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
|
||||
let pContent = message[startRange.upperBound..<endRange.lowerBound]
|
||||
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
|
||||
print(message + " + " + String(pContent))
|
||||
} else {
|
||||
print(message)
|
||||
self.finish(.failure(OperationError.SideJITIssue(error: message)))
|
||||
}
|
||||
}
|
||||
case .success():
|
||||
self.finish(.success(()))
|
||||
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
installedApp.managedObjectContext?.perform {
|
||||
var retries = 3
|
||||
while (retries > 0){
|
||||
do {
|
||||
try debug_app(installedApp.resignedBundleIdentifier)
|
||||
self.finish(.success(()))
|
||||
retries = 0
|
||||
} catch {
|
||||
retries -= 1
|
||||
if (retries <= 0){
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
|
||||
guard let udid = fetch_udid()?.toString() else {
|
||||
completion(.failure(.other("Unable to get UDID")))
|
||||
return
|
||||
}
|
||||
|
||||
var SJSURL = serverurl
|
||||
|
||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||
}
|
||||
|
||||
if !SJSURL.hasPrefix("http") {
|
||||
completion(.failure(.invalidURL))
|
||||
return
|
||||
}
|
||||
|
||||
let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
|
||||
|
||||
let url = URL(string: fullurl)!
|
||||
|
||||
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
|
||||
if let error = error {
|
||||
completion(.failure(.errorConnecting))
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
|
||||
|
||||
if datastring == "Enabled JIT for '\(installedapp.name)'!" {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "JIT Successfully Enabled"
|
||||
content.subtitle = "JIT Enabled For \(installedapp.name)"
|
||||
content.sound = UNNotificationSound.default
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
|
||||
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
completion(.success(()))
|
||||
} else {
|
||||
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
|
||||
completion(.failure(errorType))
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
@@ -1,457 +0,0 @@
|
||||
//
|
||||
// FetchAnisetteDataOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 1/7/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
import Starscream
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@objc(FetchAnisetteDataOperation)
|
||||
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
|
||||
{
|
||||
let context: OperationContext
|
||||
var socket: WebSocket!
|
||||
|
||||
var url: URL?
|
||||
var startProvisioningURL: URL?
|
||||
var endProvisioningURL: URL?
|
||||
|
||||
var clientInfo: String?
|
||||
var userAgent: String?
|
||||
|
||||
var mdLu: String?
|
||||
var deviceId: String?
|
||||
|
||||
init(context: OperationContext)
|
||||
{
|
||||
self.context = context
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
self.url = URL(string: UserDefaults.standard.menuAnisetteURL)
|
||||
print("Anisette URL: \(self.url!.absoluteString)")
|
||||
|
||||
if let identifier = Keychain.shared.identifier,
|
||||
let adiPb = Keychain.shared.adiPb {
|
||||
fetchAnisetteV3(identifier, adiPb)
|
||||
} else {
|
||||
provision()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - COMMON
|
||||
|
||||
func extractAnisetteData(_ data: Data, _ response: HTTPURLResponse?, v3: Bool) throws {
|
||||
// make sure this JSON is in the format we expect
|
||||
// convert data to json
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||
if v3 {
|
||||
if json["result"] == "GetHeadersError" {
|
||||
let message = json["message"]
|
||||
print("Error getting V3 headers: \(message ?? "no message")")
|
||||
if let message = message,
|
||||
message.contains("-45061") {
|
||||
print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
|
||||
Keychain.shared.adiPb = nil
|
||||
return provision()
|
||||
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
|
||||
}
|
||||
}
|
||||
|
||||
// try to read out a dictionary
|
||||
// for some reason serial number isn't needed but it doesn't work unless it has a value
|
||||
var formattedJSON: [String: String] = ["deviceSerialNumber": "0"]
|
||||
if let machineID = json["X-Apple-I-MD-M"] { formattedJSON["machineID"] = machineID }
|
||||
if let oneTimePassword = json["X-Apple-I-MD"] { formattedJSON["oneTimePassword"] = oneTimePassword }
|
||||
if let routingInfo = json["X-Apple-I-MD-RINFO"] { formattedJSON["routingInfo"] = routingInfo }
|
||||
|
||||
if v3 {
|
||||
formattedJSON["deviceDescription"] = self.clientInfo!
|
||||
formattedJSON["localUserID"] = self.mdLu!
|
||||
formattedJSON["deviceUniqueIdentifier"] = self.deviceId!
|
||||
|
||||
// Generate date stuff on client
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.timeZone = TimeZone.current
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||
let dateString = formatter.string(from: Date())
|
||||
formattedJSON["date"] = dateString
|
||||
formattedJSON["locale"] = Locale.current.identifier
|
||||
formattedJSON["timeZone"] = TimeZone.current.abbreviation()
|
||||
} else {
|
||||
if let deviceDescription = json["X-MMe-Client-Info"] { formattedJSON["deviceDescription"] = deviceDescription }
|
||||
if let localUserID = json["X-Apple-I-MD-LU"] { formattedJSON["localUserID"] = localUserID }
|
||||
if let deviceUniqueIdentifier = json["X-Mme-Device-Id"] { formattedJSON["deviceUniqueIdentifier"] = deviceUniqueIdentifier }
|
||||
|
||||
if let date = json["X-Apple-I-Client-Time"] { formattedJSON["date"] = date }
|
||||
if let locale = json["X-Apple-Locale"] { formattedJSON["locale"] = locale }
|
||||
if let timeZone = json["X-Apple-I-TimeZone"] { formattedJSON["timeZone"] = timeZone }
|
||||
}
|
||||
|
||||
if let response = response,
|
||||
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
|
||||
print("Implementation-Version: \(version)")
|
||||
} else { print("No Implementation-Version header") }
|
||||
|
||||
print("Anisette used: \(formattedJSON)")
|
||||
print("Original JSON: \(json)")
|
||||
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||
print("Anisette is valid!")
|
||||
self.finish(.success(anisette))
|
||||
} else {
|
||||
print("Anisette is invalid!!!!")
|
||||
if v3 {
|
||||
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||
} else {
|
||||
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if v3 {
|
||||
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not be in JSON)")
|
||||
} else {
|
||||
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not be in JSON)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - V1
|
||||
|
||||
func handleV1() {
|
||||
print("Server is V1")
|
||||
|
||||
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
|
||||
print("Server has already been trusted, fetching anisette")
|
||||
return self.fetchAnisetteV1()
|
||||
}
|
||||
|
||||
print("Alerting user about outdated server")
|
||||
let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert)
|
||||
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
|
||||
print("Fetching anisette via V1")
|
||||
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
|
||||
self.fetchAnisetteV1()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
|
||||
print("Cancelled anisette operation")
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
}))
|
||||
|
||||
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let presentingController = keyWindow?.rootViewController?.presentedViewController {
|
||||
presentingController.present(alert, animated: true)
|
||||
} else {
|
||||
keyWindow?.rootViewController?.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAnisetteV1() {
|
||||
print("Fetching anisette V1")
|
||||
URLSession.shared.dataTask(with: self.url!) { data, response, error in
|
||||
do {
|
||||
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") }
|
||||
|
||||
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false)
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
// MARK: - V3: PROVISIONING
|
||||
|
||||
func provision() {
|
||||
fetchClientInfo {
|
||||
print("Getting provisioning URLs")
|
||||
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
|
||||
request.httpMethod = "GET"
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let startProvisioningString = plist["urls"]?["midStartProvisioning"] as? String,
|
||||
let startProvisioningURL = URL(string: startProvisioningString),
|
||||
let endProvisioningString = plist["urls"]?["midFinishProvisioning"] as? String,
|
||||
let endProvisioningURL = URL(string: endProvisioningString) {
|
||||
self.startProvisioningURL = startProvisioningURL
|
||||
self.endProvisioningURL = endProvisioningURL
|
||||
print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
|
||||
print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
|
||||
print("Starting a provisioning session")
|
||||
self.startProvisioningSession()
|
||||
} else {
|
||||
print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func startProvisioningSession() {
|
||||
let provisioningSessionURL = self.url!.appendingPathComponent("v3").appendingPathComponent("provisioning_session")
|
||||
var wsRequest = URLRequest(url: provisioningSessionURL)
|
||||
wsRequest.timeoutInterval = 5
|
||||
self.socket = WebSocket(request: wsRequest)
|
||||
self.socket.delegate = self
|
||||
self.socket.connect()
|
||||
}
|
||||
|
||||
func didReceive(event: WebSocketEvent, client: WebSocketClient) {
|
||||
switch event {
|
||||
case .text(let string):
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
|
||||
guard let result = json["result"] as? String else {
|
||||
print("The server didn't give us a result")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
|
||||
return
|
||||
}
|
||||
print("Received result: \(result)")
|
||||
switch result {
|
||||
case "GiveIdentifier":
|
||||
print("Giving identifier")
|
||||
client.json(["identifier": Keychain.shared.identifier!])
|
||||
|
||||
case "GiveStartProvisioningData":
|
||||
print("Getting start provisioning data")
|
||||
let body = [
|
||||
"Header": [String: Any](),
|
||||
"Request": [String: Any](),
|
||||
]
|
||||
var request = self.buildAppleRequest(url: self.startProvisioningURL!)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let spim = plist["Response"]?["spim"] as? String {
|
||||
print("Giving start provisioning data")
|
||||
client.json(["spim": spim])
|
||||
} else {
|
||||
print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
|
||||
case "GiveEndProvisioningData":
|
||||
print("Getting end provisioning data")
|
||||
guard let cpim = json["cpim"] as? String else {
|
||||
print("The server didn't give us a cpim")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
|
||||
return
|
||||
}
|
||||
let body = [
|
||||
"Header": [String: Any](),
|
||||
"Request": [
|
||||
"cpim": cpim,
|
||||
],
|
||||
]
|
||||
var request = self.buildAppleRequest(url: self.endProvisioningURL!)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let ptm = plist["Response"]?["ptm"] as? String,
|
||||
let tk = plist["Response"]?["tk"] as? String {
|
||||
print("Giving end provisioning data")
|
||||
client.json(["ptm": ptm, "tk": tk])
|
||||
} else {
|
||||
print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
|
||||
case "ProvisioningSuccess":
|
||||
print("Provisioning succeeded!")
|
||||
client.disconnect(closeCode: 0)
|
||||
guard let adiPb = json["adi_pb"] as? String else {
|
||||
print("The server didn't give us an adi.pb file")
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
|
||||
return
|
||||
}
|
||||
Keychain.shared.adiPb = adiPb
|
||||
self.fetchAnisetteV3(Keychain.shared.identifier!, Keychain.shared.adiPb!)
|
||||
|
||||
default:
|
||||
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
|
||||
print("Failing because of \(result)")
|
||||
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch let error as NSError {
|
||||
print("Failed to handle text: \(error.localizedDescription)")
|
||||
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
|
||||
}
|
||||
|
||||
case .connected:
|
||||
print("Connected")
|
||||
|
||||
case .disconnected(let string, let code):
|
||||
print("Disconnected: \(code); \(string)")
|
||||
|
||||
case .error(let error):
|
||||
print("Got error: \(String(describing: error))")
|
||||
|
||||
default:
|
||||
print("Unknown event: \(event)")
|
||||
}
|
||||
}
|
||||
|
||||
func buildAppleRequest(url: URL) -> URLRequest {
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue(self.clientInfo!, forHTTPHeaderField: "X-Mme-Client-Info")
|
||||
request.setValue(self.userAgent!, forHTTPHeaderField: "User-Agent")
|
||||
request.setValue("text/x-xml-plist", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("*/*", forHTTPHeaderField: "Accept")
|
||||
|
||||
request.setValue(self.mdLu!, forHTTPHeaderField: "X-Apple-I-MD-LU")
|
||||
request.setValue(self.deviceId!, forHTTPHeaderField: "X-Mme-Device-Id")
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.timeZone = TimeZone(identifier: "UTC")
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||
let dateString = formatter.string(from: Date())
|
||||
request.setValue(dateString, forHTTPHeaderField: "X-Apple-I-Client-Time")
|
||||
request.setValue(Locale.current.identifier, forHTTPHeaderField: "X-Apple-Locale")
|
||||
request.setValue(TimeZone.current.abbreviation(), forHTTPHeaderField: "X-Apple-I-TimeZone")
|
||||
return request
|
||||
}
|
||||
|
||||
// MARK: - V3: FETCHING
|
||||
|
||||
func fetchClientInfo(_ callback: @escaping () -> Void) {
|
||||
if self.clientInfo != nil &&
|
||||
self.userAgent != nil &&
|
||||
self.mdLu != nil &&
|
||||
self.deviceId != nil &&
|
||||
Keychain.shared.identifier != nil {
|
||||
print("Skipping client_info fetch since all the properties we need aren't nil")
|
||||
return callback()
|
||||
}
|
||||
print("Trying to get client_info")
|
||||
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
|
||||
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
|
||||
do {
|
||||
guard let data = data, error == nil else {
|
||||
return self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The server may be down\(error != nil ? " (\(error!.localizedDescription))" : "")")))
|
||||
}
|
||||
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||
if let clientInfo = json["client_info"] {
|
||||
print("Server is V3")
|
||||
|
||||
self.clientInfo = clientInfo
|
||||
self.userAgent = json["user_agent"]!
|
||||
print("Client-Info: \(self.clientInfo!)")
|
||||
print("User-Agent: \(self.userAgent!)")
|
||||
|
||||
if Keychain.shared.identifier == nil {
|
||||
print("Generating identifier")
|
||||
var bytes = [Int8](repeating: 0, count: 16)
|
||||
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||
|
||||
if status != errSecSuccess {
|
||||
print("ERROR GENERATING IDENTIFIER!!! \(status)")
|
||||
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
|
||||
}
|
||||
|
||||
Keychain.shared.identifier = Data(bytes: &bytes, count: bytes.count).base64EncodedString()
|
||||
}
|
||||
|
||||
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
|
||||
self.mdLu = decoded.sha256().hexEncodedString()
|
||||
print("X-Apple-I-MD-LU: \(self.mdLu!)")
|
||||
let uuid: UUID = decoded.object()
|
||||
self.deviceId = uuid.uuidString.uppercased()
|
||||
print("X-Mme-Device-Id: \(self.deviceId!)")
|
||||
|
||||
callback()
|
||||
} else { self.handleV1() }
|
||||
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.handleV1()
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
||||
fetchClientInfo {
|
||||
print("Fetching anisette V3")
|
||||
let url = UserDefaults.standard.menuAnisetteURL
|
||||
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! JSONSerialization.data(withJSONObject: [
|
||||
"identifier": identifier,
|
||||
"adi_pb": adiPb
|
||||
], options: [])
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
do {
|
||||
guard let data = data, error == nil else { throw OperationError.anisetteV3Error(message: "Couldn't fetch anisette") }
|
||||
|
||||
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WebSocketClient {
|
||||
func json(_ dictionary: [String: String]) {
|
||||
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
|
||||
self.write(string: String(data: data, encoding: .utf8)!)
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
// https://stackoverflow.com/a/25391020
|
||||
func sha256() -> Data {
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
self.withUnsafeBytes {
|
||||
_ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash)
|
||||
}
|
||||
return Data(hash)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/40089462
|
||||
func hexEncodedString() -> String {
|
||||
return self.map { String(format: "%02hhX", $0) }.joined()
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/59127761
|
||||
func object<T>() -> T { self.withUnsafeBytes { $0.load(as: T.self) } }
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
//
|
||||
// InstallAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/19/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
import minimuxer
|
||||
|
||||
@objc(InstallAppOperation)
|
||||
final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
private var didCleanUp = false
|
||||
|
||||
init(context: InstallAppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 100
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let certificate = self.context.certificate,
|
||||
let resignedApp = self.context.resignedApp,
|
||||
let provisioningProfiles = self.context.provisioningProfiles
|
||||
else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("InstallAppOperation.main: self.context.certificate or self.context.resignedApp or self.context.provisioningProfiles is nil")))
|
||||
}
|
||||
|
||||
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
backgroundContext.perform {
|
||||
|
||||
|
||||
/* App */
|
||||
let installedApp: InstalledApp
|
||||
|
||||
// Fetch + update rather than insert + resolve merge conflicts to prevent potential context-level conflicts.
|
||||
if let app = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), self.context.bundleIdentifier), in: backgroundContext)
|
||||
{
|
||||
installedApp = app
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, certificateSerialNumber: certificate.serialNumber, context: backgroundContext)
|
||||
}
|
||||
|
||||
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber)
|
||||
installedApp.needsResign = false
|
||||
|
||||
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
|
||||
{
|
||||
installedApp.team = team
|
||||
}
|
||||
|
||||
/* App Extensions */
|
||||
var installedExtensions = Set<InstalledExtension>()
|
||||
|
||||
if
|
||||
let bundle = Bundle(url: resignedApp.fileURL),
|
||||
let directory = bundle.builtInPlugInsURL,
|
||||
let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
||||
{
|
||||
for case let fileURL as URL in enumerator
|
||||
{
|
||||
guard let appExtensionBundle = Bundle(url: fileURL) else { continue }
|
||||
guard let appExtension = ALTApplication(fileURL: appExtensionBundle.bundleURL) else { continue }
|
||||
|
||||
let parentBundleID = self.context.bundleIdentifier
|
||||
let resignedParentBundleID = resignedApp.bundleIdentifier
|
||||
|
||||
let resignedBundleID = appExtension.bundleIdentifier
|
||||
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
|
||||
|
||||
print("`parentBundleID`: \(parentBundleID)")
|
||||
print("`resignedParentBundleID`: \(resignedParentBundleID)")
|
||||
print("`resignedBundleID`: \(resignedBundleID)")
|
||||
print("`originalBundleID`: \(originalBundleID)")
|
||||
|
||||
let installedExtension: InstalledExtension
|
||||
|
||||
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
||||
{
|
||||
installedExtension = appExtension
|
||||
}
|
||||
else
|
||||
{
|
||||
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: backgroundContext)
|
||||
}
|
||||
|
||||
installedExtension.update(resignedAppExtension: appExtension)
|
||||
|
||||
installedExtensions.insert(installedExtension)
|
||||
}
|
||||
}
|
||||
|
||||
installedApp.appExtensions = installedExtensions
|
||||
|
||||
// Remove stale "PlugIns" (Extensions) from currently installed App
|
||||
if let installedAppExns = ALTApplication(fileURL: installedApp.fileURL)?.appExtensions {
|
||||
let currentAppExns = Set(installedApp.appExtensions).map{ $0.bundleIdentifier }
|
||||
let staleAppExns = installedAppExns.filter{ !currentAppExns.contains($0.bundleIdentifier) }
|
||||
|
||||
for staleAppExn in staleAppExns {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: staleAppExn.fileURL)
|
||||
print("InstallAppOperation.appExtensions: removed stale app-extension: \(staleAppExn.fileURL)")
|
||||
} catch {
|
||||
print("InstallAppOperation.appExtensions processing error Error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.context.beginInstallationHandler?(installedApp)
|
||||
|
||||
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
|
||||
self.cleanUp()
|
||||
|
||||
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit, provisioningProfiles.contains(where: { $1.isFreeProvisioningProfile == true })
|
||||
{
|
||||
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
|
||||
|
||||
let fetchRequest = InstalledApp.activeAppsFetchRequest()
|
||||
fetchRequest.includesPendingChanges = false
|
||||
|
||||
var activeApps = InstalledApp.fetch(fetchRequest, in: backgroundContext)
|
||||
if !activeApps.contains(installedApp)
|
||||
{
|
||||
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
|
||||
|
||||
let availableActiveApps = max(sideloadedAppsLimit - activeAppsCount, 0)
|
||||
if installedApp.requiredActiveSlots <= availableActiveApps
|
||||
{
|
||||
// This app has not been explicitly activated, but there are enough slots available,
|
||||
// so implicitly activate it.
|
||||
installedApp.isActive = true
|
||||
activeApps.append(installedApp)
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp.isActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp.isActive = true
|
||||
}
|
||||
|
||||
var installing = true
|
||||
if installedApp.storeApp?.bundleIdentifier.range(of: Bundle.Info.appbundleIdentifier) != nil {
|
||||
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
if UIApplication.shared.applicationState != .active {
|
||||
print("We are not in the foreground, let's not do anything")
|
||||
return
|
||||
}
|
||||
if !installing {
|
||||
print("Installing finished")
|
||||
return
|
||||
}
|
||||
print("We are still installing after 3 seconds")
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
switch (settings.authorizationStatus) {
|
||||
case .authorized, .ephemeral, .provisional:
|
||||
print("Notifications are enabled")
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Refreshing..."
|
||||
content.body = "SideStore will automatically move to the homescreen to finish refreshing!"
|
||||
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
|
||||
UNUserNotificationCenter.current().add(notification)
|
||||
break
|
||||
default:
|
||||
print("Notifications are not enabled")
|
||||
|
||||
let alert = UIAlertController(title: "Finish Refresh", message: "Please reopen SideStore after the process is finished.To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen manually or by hitting Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
|
||||
print("Going home")
|
||||
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||
}))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||
if var topController = keyWindow?.rootViewController {
|
||||
while let presentedViewController = topController.presentedViewController {
|
||||
topController = presentedViewController
|
||||
}
|
||||
topController.present(alert, animated: true)
|
||||
} else {
|
||||
print("No key window? Let's just go home")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try install_ipa(installedApp.bundleIdentifier)
|
||||
installing = false
|
||||
installedApp.refreshedDate = Date()
|
||||
self.finish(.success(installedApp))
|
||||
} catch let error {
|
||||
installing = false
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<InstalledApp, Error>)
|
||||
{
|
||||
self.cleanUp()
|
||||
|
||||
// Only remove refreshed IPA when finished.
|
||||
if let app = self.context.app
|
||||
{
|
||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
|
||||
do
|
||||
{
|
||||
if(FileManager.default.fileExists(atPath: fileURL.path)){
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
print("Removed refreshed IPA")
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove refreshed .ipa: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
super.finish(result)
|
||||
}
|
||||
}
|
||||
|
||||
private extension InstallAppOperation
|
||||
{
|
||||
func cleanUp()
|
||||
{
|
||||
guard !self.didCleanUp else { return }
|
||||
self.didCleanUp = true
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: self.context.temporaryDirectory)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove temporary directory.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
//
|
||||
// Operation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
class ResultOperation<ResultType>: Operation
|
||||
{
|
||||
var resultHandler: ((Result<ResultType, Error>) -> Void)?
|
||||
|
||||
// Should only be set by subclasses
|
||||
var localizedFailure: String?
|
||||
|
||||
@available(*, unavailable)
|
||||
override func finish()
|
||||
{
|
||||
super.finish()
|
||||
}
|
||||
|
||||
func finish(_ result: Result<ResultType, Error>)
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
|
||||
var result = result
|
||||
|
||||
if self.isCancelled
|
||||
{
|
||||
result = .failure(OperationError.cancelled)
|
||||
}
|
||||
else if case .failure(let nsError as NSError) = result, let localizedFailure, nsError.localizedFailure == nil {
|
||||
// Error doesn't have its own localizedFailure, so we give it the Operation's (if it exists)
|
||||
let error = nsError.withLocalizedFailure(localizedFailure)
|
||||
result = .failure(error)
|
||||
}
|
||||
self.resultHandler?(result)
|
||||
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
|
||||
class Operation: RSTOperation, ProgressReporting
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
|
||||
private var backgroundTaskID: UIBackgroundTaskIdentifier?
|
||||
|
||||
override var isAsynchronous: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
self.progress.cancellationHandler = { [weak self] in self?.cancel() }
|
||||
}
|
||||
|
||||
override func cancel()
|
||||
{
|
||||
super.cancel()
|
||||
|
||||
if !self.progress.isCancelled
|
||||
{
|
||||
self.progress.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
let name = "com.altstore." + NSStringFromClass(type(of: self))
|
||||
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: name) { [weak self] in
|
||||
guard let backgroundTask = self?.backgroundTaskID else { return }
|
||||
|
||||
self?.cancel()
|
||||
|
||||
UIApplication.shared.endBackgroundTask(backgroundTask)
|
||||
self?.backgroundTaskID = .invalid
|
||||
}
|
||||
}
|
||||
|
||||
override func finish()
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
|
||||
super.finish()
|
||||
|
||||
if let backgroundTaskID = self.backgroundTaskID
|
||||
{
|
||||
UIApplication.shared.endBackgroundTask(backgroundTaskID)
|
||||
self.backgroundTaskID = .invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
//
|
||||
// OperationError.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltSign
|
||||
import AltStoreCore
|
||||
import minimuxer
|
||||
|
||||
extension OperationError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||
typealias Error = OperationError
|
||||
|
||||
// General
|
||||
case unknown = 1000
|
||||
case unknownResult
|
||||
case cancelled
|
||||
case timedOut
|
||||
case unableToConnectSideJIT
|
||||
case unableToRespondSideJITDevice
|
||||
case wrongSideJITIP
|
||||
case SideJITIssue // (error: String)
|
||||
case refreshsidejit
|
||||
case notAuthenticated
|
||||
case appNotFound
|
||||
case unknownUDID
|
||||
case invalidApp
|
||||
case invalidParameters
|
||||
case maximumAppIDLimitReached//((application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
|
||||
case noSources
|
||||
case openAppFailed//(name: String)
|
||||
case missingAppGroup
|
||||
case refreshAppFailed
|
||||
|
||||
// Connection
|
||||
case noWiFi = 1200
|
||||
case tooNewError
|
||||
case anisetteV1Error//(message: String)
|
||||
case provisioningError//(result: String, message: String?)
|
||||
case anisetteV3Error//(message: String)
|
||||
|
||||
case cacheClearError//(errors: [String])
|
||||
}
|
||||
|
||||
static let unknownResult: OperationError = .init(code: .unknownResult)
|
||||
static let cancelled: OperationError = .init(code: .cancelled)
|
||||
static let timedOut: OperationError = .init(code: .timedOut)
|
||||
static let unableToConnectSideJIT: OperationError = .init(code: .unableToConnectSideJIT)
|
||||
static let unableToRespondSideJITDevice: OperationError = .init(code: .unableToRespondSideJITDevice)
|
||||
static let wrongSideJITIP: OperationError = .init(code: .wrongSideJITIP)
|
||||
static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
|
||||
static let unknownUDID: OperationError = .init(code: .unknownUDID)
|
||||
static let invalidApp: OperationError = .init(code: .invalidApp)
|
||||
static let noSources: OperationError = .init(code: .noSources)
|
||||
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
|
||||
|
||||
static let noWiFi: OperationError = .init(code: .noWiFi)
|
||||
static let tooNewError: OperationError = .init(code: .tooNewError)
|
||||
static let provisioningError: OperationError = .init(code: .provisioningError)
|
||||
static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error)
|
||||
static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error)
|
||||
|
||||
static let cacheClearError: OperationError = .init(code: .cacheClearError)
|
||||
|
||||
static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||
OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
||||
}
|
||||
|
||||
static func appNotFound(name: String?) -> OperationError {
|
||||
OperationError(code: .appNotFound, appName: name)
|
||||
}
|
||||
|
||||
static func openAppFailed(name: String?) -> OperationError {
|
||||
OperationError(code: .openAppFailed, appName: name)
|
||||
}
|
||||
static let domain = OperationError(code: .unknown)._domain
|
||||
|
||||
static func SideJITIssue(error: String?) -> OperationError {
|
||||
var o = OperationError(code: .SideJITIssue)
|
||||
o.errorFailure = error
|
||||
return o
|
||||
}
|
||||
|
||||
static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
|
||||
OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
||||
}
|
||||
|
||||
static func provisioningError(result: String, message: String?) -> OperationError {
|
||||
var o = OperationError(code: .provisioningError, failureReason: result)
|
||||
o.errorTitle = message
|
||||
return o
|
||||
}
|
||||
|
||||
static func cacheClearError(errors: [String]) -> OperationError {
|
||||
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
static func anisetteV1Error(message: String) -> OperationError {
|
||||
OperationError(code: .anisetteV1Error, failureReason: message)
|
||||
}
|
||||
|
||||
static func anisetteV3Error(message: String) -> OperationError {
|
||||
OperationError(code: .anisetteV3Error, failureReason: message)
|
||||
}
|
||||
|
||||
static func refreshAppFailed(message: String) -> OperationError {
|
||||
OperationError(code: .refreshAppFailed, failureReason: message)
|
||||
}
|
||||
|
||||
static func invalidParameters(_ message: String? = nil) -> OperationError {
|
||||
OperationError(code: .invalidParameters, failureReason: message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct OperationError: ALTLocalizedError {
|
||||
|
||||
let code: Code
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
var appName: String?
|
||||
|
||||
var requiredAppIDs: Int?
|
||||
var availableAppIDs: Int?
|
||||
var expirationDate: Date?
|
||||
|
||||
var sourceFile: String?
|
||||
var sourceLine: UInt?
|
||||
|
||||
private var _failureReason: String?
|
||||
|
||||
private init(code: Code, failureReason: String? = nil,
|
||||
appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil,
|
||||
expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){
|
||||
self.code = code
|
||||
self._failureReason = failureReason
|
||||
|
||||
self.appName = appName
|
||||
self.requiredAppIDs = requiredAppIDs
|
||||
self.availableAppIDs = availableAppIDs
|
||||
self.expirationDate = expirationDate
|
||||
self.sourceFile = sourceFile
|
||||
self.sourceLine = sourceLine
|
||||
}
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code {
|
||||
case .unknown:
|
||||
var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occurred.", comment: "")
|
||||
guard let sourceFile, let sourceLine else { return failureReason }
|
||||
failureReason += " (\(sourceFile) line \(sourceLine)"
|
||||
return failureReason
|
||||
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
|
||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "")
|
||||
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
|
||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
|
||||
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
|
||||
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be accessed.", comment: "")
|
||||
case .appNotFound:
|
||||
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
||||
return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
|
||||
case .openAppFailed:
|
||||
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
||||
return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName)
|
||||
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "")
|
||||
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it without SideJITServer at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "")
|
||||
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "")
|
||||
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "")
|
||||
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
|
||||
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
|
||||
case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
|
||||
case .provisioningError: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
|
||||
case .anisetteV3Error: return NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
|
||||
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
|
||||
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
|
||||
|
||||
case .refreshAppFailed:
|
||||
let message = self._failureReason ?? ""
|
||||
return String(format: NSLocalizedString("Unable to refresh App\n%@", comment: ""), message)
|
||||
|
||||
case .invalidParameters:
|
||||
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
||||
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
switch self.code
|
||||
{
|
||||
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
|
||||
case .maximumAppIDLimitReached:
|
||||
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
||||
guard let appName, let requiredAppIDs, let availableAppIDs, let expirationDate else { return baseMessage }
|
||||
var message: String
|
||||
|
||||
if requiredAppIDs > 1
|
||||
{
|
||||
let availableText: String
|
||||
|
||||
switch availableAppIDs
|
||||
{
|
||||
case 0: availableText = NSLocalizedString("none are available", comment: "")
|
||||
case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
|
||||
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
|
||||
}
|
||||
|
||||
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
|
||||
message = prefixMessage + " " + baseMessage + "\n\n"
|
||||
}
|
||||
else
|
||||
{
|
||||
message = baseMessage + " "
|
||||
}
|
||||
|
||||
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: expirationDate)
|
||||
let dateFormatter = DateComponentsFormatter()
|
||||
dateFormatter.maximumUnitCount = 1
|
||||
dateFormatter.unitsStyle = .full
|
||||
|
||||
let remainingTime = dateFormatter.string(from: dateComponents)!
|
||||
|
||||
message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
|
||||
|
||||
return message
|
||||
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MinimuxerError: LocalizedError {
|
||||
public var failureReason: String? {
|
||||
switch self {
|
||||
case .NoDevice:
|
||||
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
||||
case .NoConnection:
|
||||
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi. This could mean an invalid pairing.", comment: "")
|
||||
case .PairingFile:
|
||||
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
|
||||
|
||||
case .CreateDebug:
|
||||
return self.createService(name: "debug")
|
||||
case .LookupApps:
|
||||
return self.getFromDevice(name: "installed apps")
|
||||
case .FindApp:
|
||||
return self.getFromDevice(name: "path to the app")
|
||||
case .BundlePath:
|
||||
return self.getFromDevice(name: "bundle path")
|
||||
case .MaxPacket:
|
||||
return self.setArgument(name: "max packet")
|
||||
case .WorkingDirectory:
|
||||
return self.setArgument(name: "working directory")
|
||||
case .Argv:
|
||||
return self.setArgument(name: "argv")
|
||||
case .LaunchSuccess:
|
||||
return self.getFromDevice(name: "launch success")
|
||||
case .Detach:
|
||||
return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
||||
case .Attach:
|
||||
return NSLocalizedString("Unable to attach to the app's process", comment: "")
|
||||
|
||||
case .CreateInstproxy:
|
||||
return self.createService(name: "instproxy")
|
||||
case .CreateAfc:
|
||||
return self.createService(name: "AFC")
|
||||
case .RwAfc:
|
||||
return NSLocalizedString("AFC was unable to manage files on the device. This usually means an invalid pairing.", comment: "")
|
||||
case .InstallApp(let message):
|
||||
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "")
|
||||
case .UninstallApp:
|
||||
return NSLocalizedString("Unable to uninstall the app", comment: "")
|
||||
|
||||
case .CreateMisagent:
|
||||
return self.createService(name: "misagent")
|
||||
case .ProfileInstall:
|
||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
case .ProfileRemove:
|
||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func createService(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
|
||||
}
|
||||
|
||||
fileprivate func getFromDevice(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
|
||||
}
|
||||
|
||||
fileprivate func setArgument(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
|
||||
}
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
//
|
||||
// PatchAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/13/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import AppleArchive
|
||||
import System
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@available(iOS 14, *)
|
||||
protocol PatchAppContext
|
||||
{
|
||||
var bundleIdentifier: String { get }
|
||||
var temporaryDirectory: URL { get }
|
||||
|
||||
var resignedApp: ALTApplication? { get }
|
||||
var error: Error? { get }
|
||||
}
|
||||
|
||||
extension PatchAppError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||
typealias Error = PatchAppError
|
||||
|
||||
case unsupportedOperatingSystemVersion
|
||||
}
|
||||
|
||||
static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError {
|
||||
PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion)
|
||||
}
|
||||
}
|
||||
|
||||
struct PatchAppError: ALTLocalizedError {
|
||||
let code: Code
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
var osVersion: OperatingSystemVersion?
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code {
|
||||
case .unsupportedOperatingSystemVersion:
|
||||
let osVersionString: String
|
||||
|
||||
if let osVersion = self.osVersion?.stringValue {
|
||||
osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion
|
||||
} else {
|
||||
osVersionString = NSLocalizedString("your device's iOS version", comment: "")
|
||||
}
|
||||
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct OTAUpdate
|
||||
{
|
||||
var url: URL
|
||||
var archivePath: String
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
final class PatchAppOperation: ResultOperation<Void>
|
||||
{
|
||||
let context: PatchAppContext
|
||||
|
||||
var progressHandler: ((Progress, String) -> Void)?
|
||||
|
||||
private let appPatcher = ALTAppPatcher()
|
||||
private lazy var patchDirectory: URL = self.context.temporaryDirectory.appendingPathComponent("Patch", isDirectory: true)
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
init(context: PatchAppContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 100
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let resignedApp = self.context.resignedApp else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("PatchAppOperation.main: self.context.resignedApp is nil")))
|
||||
}
|
||||
|
||||
self.progressHandler?(self.progress, NSLocalizedString("Downloading iOS firmware...", comment: ""))
|
||||
|
||||
self.cancellable = self.fetchOTAUpdate()
|
||||
.flatMap { self.downloadArchive(from: $0) }
|
||||
.flatMap { self.extractSpotlightFromArchive(at: $0) }
|
||||
.flatMap { self.patch(resignedApp, withBinaryAt: $0) }
|
||||
.tryMap { try FileManager.default.zipAppBundle(at: $0) }
|
||||
.tryMap { (fileURL) in
|
||||
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
|
||||
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
|
||||
}
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { completion in
|
||||
switch completion
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .finished: self.finish(.success(()))
|
||||
}
|
||||
} receiveValue: { _ in }
|
||||
}
|
||||
|
||||
override func cancel()
|
||||
{
|
||||
super.cancel()
|
||||
|
||||
self.cancellable?.cancel()
|
||||
self.cancellable = nil
|
||||
}
|
||||
}
|
||||
|
||||
private let ALTFragmentZipCallback: @convention(c) (UInt32) -> Void = { (percentageComplete) in
|
||||
guard let progress = Progress.current() else { return }
|
||||
|
||||
if percentageComplete == 100 && progress.completedUnitCount == 0
|
||||
{
|
||||
// Ignore first percentageComplete, which is always 100.
|
||||
return
|
||||
}
|
||||
|
||||
progress.completedUnitCount = Int64(percentageComplete)
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
private extension PatchAppOperation
|
||||
{
|
||||
func fetchOTAUpdate() -> AnyPublisher<OTAUpdate, Error>
|
||||
{
|
||||
Just(()).tryMap {
|
||||
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
|
||||
switch (osVersion.majorVersion, osVersion.minorVersion)
|
||||
{
|
||||
case (14, 3):
|
||||
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2020WinterFCS/patches/001-87330/99E29969-F6B6-422A-B946-70DE2E2D73BE/com_apple_MobileAsset_SoftwareUpdate/67f9e42f5e57a20e0a87eaf81b69dd2a61311d3f.zip")!,
|
||||
archivePath: "AssetData/payloadv2/payload.042")
|
||||
|
||||
case (14, 4):
|
||||
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021WinterFCS/patches/001-98606/43AF99A1-F286-43B1-A101-F9F856EA395A/com_apple_MobileAsset_SoftwareUpdate/c4985c32c344beb7b49c61919b4e39d1fd336c90.zip")!,
|
||||
archivePath: "AssetData/payloadv2/payload.042")
|
||||
|
||||
case (14, 5):
|
||||
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021SpringFCS/patches/061-84483/AB525139-066E-46F8-8E85-DCE802C03BA8/com_apple_MobileAsset_SoftwareUpdate/788573ae93113881db04269acedeecabbaa643e3.zip")!,
|
||||
archivePath: "AssetData/payloadv2/payload.043")
|
||||
|
||||
default: throw PatchAppError.unsupportedOperatingSystemVersion(osVersion)
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func downloadArchive(from update: OTAUpdate) -> AnyPublisher<URL, Error>
|
||||
{
|
||||
Just(()).tryMap {
|
||||
#if targetEnvironment(simulator)
|
||||
throw PatchAppError.unsupportedOperatingSystemVersion(ProcessInfo.processInfo.operatingSystemVersion)
|
||||
#else
|
||||
|
||||
try FileManager.default.createDirectory(at: self.patchDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let archiveURL = self.patchDirectory.appendingPathComponent("ota.archive")
|
||||
try archiveURL.withUnsafeFileSystemRepresentation { archivePath in
|
||||
guard let fz = fragmentzip_open((update.url.absoluteString as NSString).utf8String!) else {
|
||||
throw URLError(.cannotConnectToHost, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("The connection failed because a connection cannot be made to the host.", comment: ""),
|
||||
NSURLErrorKey: update.url])
|
||||
}
|
||||
defer { fragmentzip_close(fz) }
|
||||
|
||||
self.progress.becomeCurrent(withPendingUnitCount: 100)
|
||||
defer { self.progress.resignCurrent() }
|
||||
|
||||
guard fragmentzip_download_file(fz, update.archivePath, archivePath!, ALTFragmentZipCallback) == 0 else {
|
||||
throw URLError(.networkConnectionLost, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("The connection failed because the network connection was lost.", comment: ""),
|
||||
NSURLErrorKey: update.url])
|
||||
}
|
||||
}
|
||||
|
||||
print("Downloaded OTA archive.")
|
||||
return archiveURL
|
||||
|
||||
#endif
|
||||
}
|
||||
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not download OTA archive.", comment: "")) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func extractSpotlightFromArchive(at archiveURL: URL) -> AnyPublisher<URL, Error>
|
||||
{
|
||||
Just(()).tryMap {
|
||||
#if targetEnvironment(simulator)
|
||||
throw PatchAppError.unsupportedOperatingSystemVersion(ProcessInfo.processInfo.operatingSystemVersion)
|
||||
#else
|
||||
|
||||
let spotlightPath = "Applications/Spotlight.app/Spotlight"
|
||||
let spotlightFileURL = self.patchDirectory.appendingPathComponent(spotlightPath)
|
||||
|
||||
guard let readFileStream = ArchiveByteStream.fileStream(path: FilePath(archiveURL.path), mode: .readOnly, options: [], permissions: FilePermissions(rawValue: 0o644)),
|
||||
let decompressStream = ArchiveByteStream.decompressionStream(readingFrom: readFileStream),
|
||||
let decodeStream = ArchiveStream.decodeStream(readingFrom: decompressStream),
|
||||
let readStream = ArchiveStream.extractStream(extractingTo: FilePath(self.patchDirectory.path))
|
||||
else { throw CocoaError(.fileReadCorruptFile, userInfo: [NSURLErrorKey: archiveURL]) }
|
||||
|
||||
_ = try ArchiveStream.process(readingFrom: decodeStream, writingTo: readStream) { message, filePath, data in
|
||||
guard filePath == FilePath(spotlightPath) else { return .skip }
|
||||
return .ok
|
||||
}
|
||||
|
||||
print("Extracted Spotlight from OTA archive.")
|
||||
return spotlightFileURL
|
||||
|
||||
#endif
|
||||
}
|
||||
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not extract Spotlight from OTA archive.", comment: "")) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func patch(_ app: ALTApplication, withBinaryAt patchFileURL: URL) -> AnyPublisher<URL, Error>
|
||||
{
|
||||
Just(()).tryMap {
|
||||
// executableURL may be nil, so use infoDictionary instead to determine executable name.
|
||||
// guard let appName = app.bundle.executableURL?.lastPathComponent else { throw OperationError.invalidApp }
|
||||
guard let appName = app.bundle.infoDictionary?[kCFBundleExecutableKey as String] as? String else { throw OperationError.invalidApp }
|
||||
|
||||
let temporaryAppURL = self.patchDirectory.appendingPathComponent("Patched.app", isDirectory: true)
|
||||
try FileManager.default.copyItem(at: app.fileURL, to: temporaryAppURL)
|
||||
|
||||
let appBinaryURL = temporaryAppURL.appendingPathComponent(appName, isDirectory: false)
|
||||
try self.appPatcher.patchAppBinary(at: appBinaryURL, withBinaryAt: patchFileURL)
|
||||
|
||||
print("Patched \(app.name).")
|
||||
return temporaryAppURL
|
||||
}
|
||||
.mapError { ($0 as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), app.name)) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
@@ -1,499 +0,0 @@
|
||||
//
|
||||
// PatchViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/20/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension PatchViewController
|
||||
{
|
||||
enum Step
|
||||
{
|
||||
case confirm
|
||||
case install
|
||||
case openApp
|
||||
case patchApp
|
||||
case reboot
|
||||
case refresh
|
||||
case finish
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
final class PatchViewController: UIViewController
|
||||
{
|
||||
var patchApp: AnyApp?
|
||||
var installedApp: InstalledApp?
|
||||
|
||||
var completionHandler: ((Result<Void, Error>) -> Void)?
|
||||
|
||||
private let context = AuthenticatedOperationContext()
|
||||
|
||||
private var currentStep: Step = .confirm {
|
||||
didSet {
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var buttonHandler: (() -> Void)?
|
||||
private var resignedApp: ALTApplication?
|
||||
|
||||
private lazy var temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
private var didEnterBackgroundObservation: NSObjectProtocol?
|
||||
private weak var cancellableProgress: Progress?
|
||||
|
||||
@IBOutlet private var placeholderView: RSTPlaceholderView!
|
||||
@IBOutlet private var taskDescriptionLabel: UILabel!
|
||||
@IBOutlet private var pillButton: PillButton!
|
||||
@IBOutlet private var cancelBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet private var cancelButton: UIButton!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.isModalInPresentation = true
|
||||
|
||||
self.placeholderView.stackView.spacing = 20
|
||||
self.placeholderView.textLabel.textColor = .white
|
||||
|
||||
self.placeholderView.detailTextLabel.textAlignment = .left
|
||||
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
self?.startProcess()
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to create temporary directory:", error)
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if self.installedApp != nil
|
||||
{
|
||||
self.refreshApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private extension PatchViewController
|
||||
{
|
||||
func update()
|
||||
{
|
||||
self.cancelButton.alpha = 0.0
|
||||
|
||||
switch self.currentStep
|
||||
{
|
||||
case .confirm:
|
||||
guard let app = self.patchApp else { break }
|
||||
|
||||
if UIDevice.current.isUntetheredJailbreakRequired
|
||||
{
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Requires Untethering", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak is untethered, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but SideStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Supports Untethering", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak has an untethered version, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but SideStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
|
||||
}
|
||||
|
||||
self.pillButton.setTitle(NSLocalizedString("Install Untethered Jailbreak", comment: ""), for: .normal)
|
||||
|
||||
self.cancelButton.alpha = 1.0
|
||||
|
||||
case .install:
|
||||
guard let app = self.patchApp else { break }
|
||||
|
||||
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Installing %@ placeholder…", comment: ""), app.name)
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("A placeholder app needs to be installed in order to prepare your device for untethering.\n\nThis may take a few moments.", comment: "")
|
||||
|
||||
case .openApp:
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
|
||||
|
||||
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
|
||||
|
||||
case .patchApp:
|
||||
guard let app = self.patchApp else { break }
|
||||
|
||||
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Patching %@ placeholder…", comment: ""), app.name)
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("This will take a few moments. Please do not turn off the screen or leave the app until patching is complete.", comment: "")
|
||||
|
||||
self.pillButton.setTitle(NSLocalizedString("Patch Placeholder", comment: ""), for: .normal)
|
||||
|
||||
case .reboot:
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
|
||||
|
||||
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
|
||||
|
||||
case .refresh:
|
||||
guard let installedApp = self.installedApp else { break }
|
||||
|
||||
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish installing %@?", comment: ""), installedApp.name)
|
||||
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("In order to finish jailbreaking this device, you need to install %@ then follow the instructions in the app.", comment: ""), installedApp.name)
|
||||
|
||||
self.pillButton.setTitle(String(format: NSLocalizedString("Install %@", comment: ""), installedApp.name), for: .normal)
|
||||
|
||||
case .finish:
|
||||
guard let installedApp = self.installedApp else { break }
|
||||
|
||||
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish in %@", comment: ""), installedApp.name)
|
||||
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("Follow the instructions in %@ to finish jailbreaking this device.", comment: ""), installedApp.name)
|
||||
|
||||
self.pillButton.setTitle(String(format: NSLocalizedString("Open %@", comment: ""), installedApp.name), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
func present(_ error: Error, title: String)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let nsError = error as NSError
|
||||
|
||||
let alertController = UIAlertController(title: nsError.localizedFailure ?? title, message: error.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
|
||||
self.setProgress(nil, description: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func setProgress(_ progress: Progress?, description: String?)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.pillButton.progress = progress
|
||||
self.taskDescriptionLabel.text = description ?? " " // Use non-empty string to prevent label resizing itself.
|
||||
}
|
||||
}
|
||||
|
||||
func finish(with result: Result<Void, Error>)
|
||||
{
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove temporary directory:", error)
|
||||
}
|
||||
|
||||
if let observation = self.didEnterBackgroundObservation
|
||||
{
|
||||
NotificationCenter.default.removeObserver(observation)
|
||||
}
|
||||
|
||||
self.completionHandler?(result)
|
||||
self.completionHandler = nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private extension PatchViewController
|
||||
{
|
||||
@IBAction func performButtonAction()
|
||||
{
|
||||
self.buttonHandler?()
|
||||
}
|
||||
|
||||
@IBAction func cancel()
|
||||
{
|
||||
self.finish(with: .success(()))
|
||||
|
||||
self.cancellableProgress?.cancel()
|
||||
}
|
||||
|
||||
@IBAction func installRegularJailbreak()
|
||||
{
|
||||
guard let app = self.patchApp else { return }
|
||||
|
||||
let title: String
|
||||
let message: String
|
||||
|
||||
if UIDevice.current.isUntetheredJailbreakRequired
|
||||
{
|
||||
title = NSLocalizedString("Untethering Required", comment: "")
|
||||
message = String(format: NSLocalizedString("%@ can not jailbreak this device unless you untether it first. Are you sure you want to install without untethering?", comment: ""), app.name)
|
||||
}
|
||||
else
|
||||
{
|
||||
title = NSLocalizedString("Untethering Recommended", comment: "")
|
||||
message = String(format: NSLocalizedString("Untethering this jailbreak will prevent %@ from expiring, even after 7 days or rebooting the device. Are you sure you want to install without untethering?", comment: ""), app.name)
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Install Without Untethering", comment: ""), style: .default) { _ in
|
||||
self.finish(with: .failure(OperationError.cancelled))
|
||||
})
|
||||
alertController.addAction(.cancel)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private extension PatchViewController
|
||||
{
|
||||
func startProcess()
|
||||
{
|
||||
guard let patchApp = self.patchApp else { return }
|
||||
|
||||
self.currentStep = .install
|
||||
|
||||
if let progress = AppManager.shared.installationProgress(for: patchApp)
|
||||
{
|
||||
// Cancel pending jailbreak app installation so we can start a new one.
|
||||
progress.cancel()
|
||||
}
|
||||
|
||||
let appURL = InstalledApp.fileURL(for: patchApp)
|
||||
let cachedAppURL = self.temporaryDirectory.appendingPathComponent("Cached.app")
|
||||
|
||||
do
|
||||
{
|
||||
// Make copy of original app, so we can replace the cached patch app with it later.
|
||||
try FileManager.default.copyItem(at: appURL, to: cachedAppURL, shouldReplace: true)
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.present(error, title: NSLocalizedString("Could not back up jailbreak app.", comment: ""))
|
||||
return
|
||||
}
|
||||
|
||||
var unzippingError: Error?
|
||||
let refreshGroup = AppManager.shared.install(patchApp, presentingViewController: self, context: self.context) { result in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
|
||||
if let unzippingError = unzippingError
|
||||
{
|
||||
throw unzippingError
|
||||
}
|
||||
|
||||
// Replace cached patch app with original app so we can resume installing it post-reboot.
|
||||
try FileManager.default.copyItem(at: cachedAppURL, to: appURL, shouldReplace: true)
|
||||
|
||||
self.openApp()
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.present(error, title: String(format: NSLocalizedString("Could not install %@ placeholder.", comment: ""), patchApp.name))
|
||||
}
|
||||
}
|
||||
refreshGroup.beginInstallationHandler = { (installedApp) in
|
||||
do
|
||||
{
|
||||
// Replace patch app name with correct name.
|
||||
installedApp.name = patchApp.name
|
||||
|
||||
let ipaURL = installedApp.refreshedIPAURL
|
||||
let resignedAppURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: self.temporaryDirectory)
|
||||
|
||||
self.resignedApp = ALTApplication(fileURL: resignedAppURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error unzipping app bundle:", error)
|
||||
unzippingError = error
|
||||
}
|
||||
}
|
||||
self.setProgress(refreshGroup.progress, description: nil)
|
||||
|
||||
self.cancellableProgress = refreshGroup.progress
|
||||
}
|
||||
|
||||
func openApp()
|
||||
{
|
||||
guard let patchApp = self.patchApp else { return }
|
||||
|
||||
self.setProgress(nil, description: nil)
|
||||
self.currentStep = .openApp
|
||||
|
||||
// This observation is willEnterForeground because patching starts immediately upon return.
|
||||
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { (notification) in
|
||||
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
|
||||
self.patchApplication()
|
||||
}
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
|
||||
let openURL = InstalledApp.openAppURL(for: patchApp)
|
||||
UIApplication.shared.open(openURL) { success in
|
||||
guard !success else { return }
|
||||
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func patchApplication()
|
||||
{
|
||||
guard let resignedApp = self.resignedApp else { return }
|
||||
|
||||
self.currentStep = .patchApp
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
self?.patchApplication()
|
||||
}
|
||||
|
||||
let patchAppOperation = AppManager.shared.patch(resignedApp: resignedApp, presentingViewController: self, context: self.context) { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.present(error, title: String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), resignedApp.name))
|
||||
case .success: self.rebootDevice()
|
||||
}
|
||||
}
|
||||
patchAppOperation.progressHandler = { (progress, description) in
|
||||
self.setProgress(progress, description: description)
|
||||
}
|
||||
self.cancellableProgress = patchAppOperation.progress
|
||||
}
|
||||
|
||||
func rebootDevice()
|
||||
{
|
||||
guard let patchApp = self.patchApp else { return }
|
||||
|
||||
self.setProgress(nil, description: nil)
|
||||
self.currentStep = .reboot
|
||||
|
||||
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
|
||||
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
|
||||
|
||||
var patchedApps = UserDefaults.standard.patchedApps ?? []
|
||||
if !patchedApps.contains(patchApp.bundleIdentifier)
|
||||
{
|
||||
patchedApps.append(patchApp.bundleIdentifier)
|
||||
UserDefaults.standard.patchedApps = patchedApps
|
||||
}
|
||||
|
||||
self.finish(with: .success(()))
|
||||
}
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
|
||||
let openURL = InstalledApp.openAppURL(for: patchApp)
|
||||
UIApplication.shared.open(openURL) { success in
|
||||
guard !success else { return }
|
||||
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func refreshApp()
|
||||
{
|
||||
guard let installedApp = self.installedApp else { return }
|
||||
|
||||
self.currentStep = .refresh
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||
tempApp.needsResign = true
|
||||
|
||||
let errorTitle = String(format: NSLocalizedString("Could not install %@.", comment: ""), tempApp.name)
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
// Refreshing ensures we don't attempt to patch the app again,
|
||||
// since that is only checked when installing a new app.
|
||||
let refreshGroup = AppManager.shared.refresh([installedApp], presentingViewController: self, group: nil)
|
||||
refreshGroup.completionHandler = { [weak refreshGroup, weak self] (results) in
|
||||
guard let self = self else { return }
|
||||
|
||||
do
|
||||
{
|
||||
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown() }
|
||||
_ = try result.get()
|
||||
|
||||
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
|
||||
{
|
||||
patchedApps.remove(at: index)
|
||||
UserDefaults.standard.patchedApps = patchedApps
|
||||
}
|
||||
|
||||
self.finish()
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.present(error, title: errorTitle)
|
||||
}
|
||||
}
|
||||
self.setProgress(refreshGroup.progress, description: String(format: NSLocalizedString("Installing %@...", comment: ""), installedApp.name))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.present(error, title: errorTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func finish()
|
||||
{
|
||||
guard let installedApp = self.installedApp else { return }
|
||||
|
||||
self.setProgress(nil, description: nil)
|
||||
self.currentStep = .finish
|
||||
|
||||
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
|
||||
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
|
||||
self.finish(with: .success(()))
|
||||
}
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
let appName = installedApp.name
|
||||
let openURL = installedApp.openAppURL
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
|
||||
UIApplication.shared.open(openURL) { success in
|
||||
guard !success else { return }
|
||||
self.present(OperationError.openAppFailed(name: appName), title: String(format: NSLocalizedString("Could not open %@.", comment: ""), appName))
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// fragmentzip.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/25/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef fragmentzip_h
|
||||
#define fragmentzip_h
|
||||
|
||||
typedef void fragmentzip_t;
|
||||
typedef void (*fragmentzip_process_callback_t)(unsigned int progress);
|
||||
fragmentzip_t *fragmentzip_open(const char *url);
|
||||
int fragmentzip_download_file(fragmentzip_t *info, const char *remotepath, const char *savepath, fragmentzip_process_callback_t callback);
|
||||
void fragmentzip_close(fragmentzip_t *info);
|
||||
|
||||
#endif /* fragmentzip_h */
|
||||
@@ -1,78 +0,0 @@
|
||||
//
|
||||
// RefreshAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 2/27/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
import minimuxer
|
||||
|
||||
@objc(RefreshAppOperation)
|
||||
final class RefreshAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
// Strong reference to managedObjectContext to keep it alive until we're finished.
|
||||
let managedObjectContext: NSManagedObjectContext
|
||||
|
||||
init(context: AppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
self.managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
do
|
||||
{
|
||||
if let error = self.context.error {
|
||||
print("RefreshAppOperation.main: ERROR: self.context.app = \(self.context.app!); self.context.error is \(error)")
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
|
||||
guard let profiles = self.context.provisioningProfiles else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("RefreshAppOperation.main: self.context.provisioningProfiles is nil")))
|
||||
}
|
||||
|
||||
guard let app = self.context.app else { return self.finish(.failure(OperationError(.appNotFound(name: nil)))) }
|
||||
|
||||
for p in profiles {
|
||||
do {
|
||||
let bytes = p.value.data.toRustByteSlice()
|
||||
try install_provisioning_profile(bytes.forRust())
|
||||
} catch {
|
||||
self.finish(.failure(MinimuxerError.ProfileInstall))
|
||||
}
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
||||
self.managedObjectContext.perform {
|
||||
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
|
||||
self.finish(.failure(OperationError(.appNotFound(name: app.name))))
|
||||
return
|
||||
}
|
||||
installedApp.update(provisioningProfile: p.value)
|
||||
for installedExtension in installedApp.appExtensions {
|
||||
guard let provisioningProfile = profiles[installedExtension.bundleIdentifier] else { continue }
|
||||
installedExtension.update(provisioningProfile: provisioningProfile)
|
||||
}
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// RemoveAppBackupOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/13/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc(RemoveAppBackupOperation)
|
||||
final class RemoveAppBackupOperation: ResultOperation<Void>
|
||||
{
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
private let coordinator = NSFileCoordinator()
|
||||
private let coordinatorQueue = OperationQueue()
|
||||
|
||||
init(context: InstallAppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.coordinatorQueue.name = "AltStore - RemoveAppBackupOperation Queue"
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let installedApp = self.context.installedApp else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("RemoveAppBackupOperation.main: self.context.installedApp is nil")))
|
||||
}
|
||||
installedApp.managedObjectContext?.perform {
|
||||
guard let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return self.finish(.failure(OperationError.missingAppGroup)) }
|
||||
|
||||
let intent = NSFileAccessIntent.writingIntent(with: backupDirectoryURL, options: [.forDeleting])
|
||||
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
try FileManager.default.removeItem(at: intent.url)
|
||||
|
||||
self.finish(.success(()))
|
||||
}
|
||||
catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile
|
||||
{
|
||||
#if DEBUG
|
||||
|
||||
// When debugging, it's expected that app groups don't match, so ignore.
|
||||
self.finish(.success(()))
|
||||
|
||||
#else
|
||||
|
||||
print("Failed to remove app backup directory:", error)
|
||||
self.finish(.failure(error))
|
||||
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove app backup directory:", error)
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// SendAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import minimuxer
|
||||
|
||||
@objc(SendAppOperation)
|
||||
final class SendAppOperation: ResultOperation<()>
|
||||
{
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "com.sidestore.SendAppOperation")
|
||||
|
||||
init(context: InstallAppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 1
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
|
||||
guard let resignedApp = self.context.resignedApp else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("SendAppOperation.main: self.resignedApp is nil")))
|
||||
}
|
||||
|
||||
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
||||
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
|
||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
|
||||
print("AFC App `fileURL`: \(fileURL.absoluteString)")
|
||||
|
||||
if let data = NSData(contentsOf: fileURL) {
|
||||
do {
|
||||
let bytes = Data(data).toRustByteSlice()
|
||||
try yeet_app_afc(app.bundleIdentifier, bytes.forRust())
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(()))
|
||||
} catch {
|
||||
self.finish(.failure(MinimuxerError.RwAfc))
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(()))
|
||||
}
|
||||
} else {
|
||||
print("IPA doesn't exist????")
|
||||
self.finish(.failure(OperationError(.appNotFound(name: resignedApp.name))))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
//
|
||||
// VerifyAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/2/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
extension VerificationError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||
typealias Error = VerificationError
|
||||
|
||||
case privateEntitlements
|
||||
case mismatchedBundleIdentifiers
|
||||
case iOSVersionNotSupported
|
||||
}
|
||||
|
||||
static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError {
|
||||
VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements)
|
||||
}
|
||||
|
||||
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
||||
VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID)
|
||||
}
|
||||
|
||||
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
|
||||
VerificationError(code: .iOSVersionNotSupported, app: app)
|
||||
}
|
||||
}
|
||||
|
||||
struct VerificationError: ALTLocalizedError {
|
||||
let code: Code
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
@Managed var app: AppProtocol?
|
||||
var entitlements: [String: Any]?
|
||||
var sourceBundleID: String?
|
||||
var deviceOSVersion: OperatingSystemVersion?
|
||||
var requiredOSVersion: OperatingSystemVersion?
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self.code {
|
||||
case .iOSVersionNotSupported:
|
||||
guard let deviceOSVersion else { return nil }
|
||||
|
||||
var failureReason = self.errorFailureReason
|
||||
if self.app == nil {
|
||||
let firstLetter = failureReason.prefix(1).lowercased()
|
||||
failureReason = firstLetter + failureReason.dropFirst()
|
||||
}
|
||||
|
||||
return String(formatted: "This device is running iOS %@, but %@", deviceOSVersion.stringValue, failureReason)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code
|
||||
{
|
||||
case .privateEntitlements:
|
||||
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||
return String(formatted: "“%@” requires private permissions.", appName)
|
||||
|
||||
case .mismatchedBundleIdentifiers:
|
||||
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID {
|
||||
return String(formatted: "The bundle ID '%@' does not match the one specified by the source ('%@').", appBundleID, bundleID)
|
||||
} else {
|
||||
return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
|
||||
}
|
||||
|
||||
case .iOSVersionNotSupported:
|
||||
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||
let deviceOSVersion = self.deviceOSVersion ?? ProcessInfo.processInfo.operatingSystemVersion
|
||||
|
||||
guard let requiredOSVersion else {
|
||||
return String(formatted: "%@ does not support iOS %@.", appName, deviceOSVersion.stringValue)
|
||||
}
|
||||
if deviceOSVersion > requiredOSVersion {
|
||||
return String(formatted: "%@ requires iOS %@ or earlier", appName, requiredOSVersion.stringValue)
|
||||
} else {
|
||||
return String(formatted: "%@ requires iOS %@ or later", appName, requiredOSVersion.stringValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(VerifyAppOperation)
|
||||
final class VerifyAppOperation: ResultOperation<Void>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
var verificationHandler: ((VerificationError) -> Bool)?
|
||||
|
||||
init(context: AppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
do
|
||||
{
|
||||
if let error = self.context.error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
let appName = self.context.app?.name ?? NSLocalizedString("The app", comment: "")
|
||||
self.localizedFailure = String(format: NSLocalizedString("%@ could not be installed.", comment: ""), appName)
|
||||
|
||||
guard let app = self.context.app else {
|
||||
throw OperationError.invalidParameters("VerifyAppOperation.main: self.context.app is nil")
|
||||
}
|
||||
|
||||
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
|
||||
guard app.bundleIdentifier == self.context.bundleIdentifier else {
|
||||
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
|
||||
}
|
||||
}
|
||||
|
||||
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
|
||||
throw VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: app.minimumiOSVersion)
|
||||
}
|
||||
|
||||
if #available(iOS 13.5, *)
|
||||
{
|
||||
// No psychic paper, so we can ignore private entitlements
|
||||
app.hasPrivateEntitlements = false
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure this goes last, since once user responds to alert we don't do any more app verification.
|
||||
if let commentStart = app.entitlementsString.range(of: "<!---><!-->"), let commentEnd = app.entitlementsString.range(of: "<!-- -->")
|
||||
{
|
||||
// Psychic Paper private entitlements.
|
||||
|
||||
let entitlementsStart = app.entitlementsString.index(after: commentStart.upperBound)
|
||||
let rawEntitlements = String(app.entitlementsString[entitlementsStart ..< commentEnd.lowerBound])
|
||||
|
||||
let plistTemplate = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
%@
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
let entitlementsPlist = String(format: plistTemplate, rawEntitlements)
|
||||
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
|
||||
|
||||
app.hasPrivateEntitlements = true
|
||||
let error = VerificationError.privateEntitlements(entitlements, app: app)
|
||||
self.process(error) { (result) in
|
||||
self.finish(result.mapError { $0 as Error })
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
else
|
||||
{
|
||||
app.hasPrivateEntitlements = false
|
||||
}
|
||||
}
|
||||
|
||||
self.finish(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension VerifyAppOperation
|
||||
{
|
||||
func process(_ error: VerificationError, completion: @escaping (Result<Void, VerificationError>) -> Void)
|
||||
{
|
||||
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
switch error.code
|
||||
{
|
||||
case .privateEntitlements:
|
||||
guard let entitlements = error.entitlements else { return completion(.failure(error)) }
|
||||
let permissions = entitlements.keys.sorted().joined(separator: "\n")
|
||||
let message = String(format: NSLocalizedString("""
|
||||
You must allow access to these private permissions before continuing:
|
||||
|
||||
%@
|
||||
|
||||
Private permissions allow apps to do more than normally allowed by iOS, including potentially accessing sensitive private data. Make sure to only install apps from sources you trust.
|
||||
""", comment: ""), permissions)
|
||||
|
||||
let alertController = UIAlertController(title: error.failureReason ?? error.localizedDescription, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Allow Access", comment: ""), style: .destructive) { (action) in
|
||||
completion(.success(()))
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Deny Access", comment: ""), style: .default, handler: { (action) in
|
||||
completion(.failure(error))
|
||||
}))
|
||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||
|
||||
case .mismatchedBundleIdentifiers, .iOSVersionNotSupported: return completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,158 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "57.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "114.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "50.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "50x50"
|
||||
},
|
||||
{
|
||||
"filename" : "100.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "50x50"
|
||||
},
|
||||
{
|
||||
"filename" : "72.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "72x72"
|
||||
},
|
||||
{
|
||||
"filename" : "144.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "72x72"
|
||||
},
|
||||
{
|
||||
"filename" : "76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 846 KiB |
@@ -1,220 +0,0 @@
|
||||
//
|
||||
// AnisetteServerList.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by ny on 6/18/24.
|
||||
// Copyright © 2024 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
typealias SUIButton = SwiftUI.Button
|
||||
|
||||
// MARK: - AnisetteServerData
|
||||
struct AnisetteServerData: Codable {
|
||||
let servers: [Server]
|
||||
}
|
||||
|
||||
// MARK: - Server
|
||||
struct Server: Codable {
|
||||
var name: String
|
||||
var address: String
|
||||
}
|
||||
|
||||
class AnisetteViewModel: ObservableObject {
|
||||
@Published var selected: String = ""
|
||||
|
||||
@Published var source: String = "https://servers.sidestore.io/servers.json"
|
||||
@Published var servers: [Server] = []
|
||||
|
||||
init() {
|
||||
// using the custom Anisette list
|
||||
if !UserDefaults.standard.menuAnisetteList.isEmpty {
|
||||
self.source = UserDefaults.standard.menuAnisetteList
|
||||
}
|
||||
}
|
||||
|
||||
func getListOfServers() {
|
||||
guard let url = URL(string: source) else { return }
|
||||
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
return
|
||||
}
|
||||
if let data = data {
|
||||
do {
|
||||
let decoder = Foundation.JSONDecoder()
|
||||
let servers = try decoder.decode(AnisetteServerData.self, from: data)
|
||||
DispatchQueue.main.async {
|
||||
self.servers = servers.servers
|
||||
}
|
||||
} catch {
|
||||
// Handle decoding error
|
||||
print("Failed to decode JSON: \(error)")
|
||||
}
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
struct AnisetteServers: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@StateObject var viewModel: AnisetteViewModel = AnisetteViewModel()
|
||||
@State var selected: String? = nil
|
||||
@State private var showingConfirmation = false
|
||||
var errorCallback: () -> ()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
viewModel.getListOfServers()
|
||||
}
|
||||
VStack {
|
||||
if #available(iOS 16.0, *) {
|
||||
SwiftUI.List($viewModel.servers, id: \.address, selection: $selected) { server in
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text("\(server.name.wrappedValue)")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Text("\(server.address.wrappedValue)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if selected != nil {
|
||||
if server.address.wrappedValue == selected {
|
||||
Spacer()
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.onAppear {
|
||||
UserDefaults.standard.menuAnisetteURL = server.address.wrappedValue
|
||||
print(UserDefaults.synchronize(.standard)())
|
||||
print(UserDefaults.standard.menuAnisetteURL)
|
||||
print(server.address.wrappedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color(UIColor.secondarySystemBackground)))
|
||||
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.listRowBackground(Color(UIColor.systemBackground))
|
||||
} else {
|
||||
List(selection: $selected) {
|
||||
ForEach($viewModel.servers, id: \.name) { server in
|
||||
VStack {
|
||||
HStack {
|
||||
Text("\(server.name.wrappedValue)")
|
||||
.foregroundColor(.primary)
|
||||
.frame(alignment: .center)
|
||||
Text("\(server.address.wrappedValue)")
|
||||
.foregroundColor(.secondary)
|
||||
.frame(alignment: .center)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color(UIColor.secondarySystemBackground)))
|
||||
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
|
||||
VStack(spacing: 16) {
|
||||
TextField("Anisette Server List", text: $viewModel.source)
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color(UIColor.secondarySystemFill)))
|
||||
.foregroundColor(.primary)
|
||||
.frame(height: 60)
|
||||
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
|
||||
.onChange(of: viewModel.source) { newValue in
|
||||
UserDefaults.standard.menuAnisetteList = newValue
|
||||
viewModel.getListOfServers()
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
SUIButton(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}) {
|
||||
HStack{
|
||||
Spacer()
|
||||
Text("Back")
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor))
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: Color.accentColor.opacity(0.4), radius: 10, x: 0, y: 5)
|
||||
|
||||
SUIButton(action: {
|
||||
viewModel.getListOfServers()
|
||||
}) {
|
||||
HStack{
|
||||
Spacer()
|
||||
Text("Refresh Servers")
|
||||
.fontWeight(.semibold)
|
||||
.frame(maxWidth: .infinity)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor))
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: Color.accentColor.opacity(0.4), radius: 10, x: 0, y: 5)
|
||||
|
||||
}
|
||||
|
||||
SUIButton(action: {
|
||||
showingConfirmation = true
|
||||
}) {
|
||||
Text("Reset adi.pb")
|
||||
.fontWeight(.semibold)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.red))
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: Color.red.opacity(0.4), radius: 10, x: 0, y: 5)
|
||||
.alert(isPresented: $showingConfirmation) {
|
||||
Alert(
|
||||
title: Text("Reset adi.pb"),
|
||||
message: Text("are you sure to clear the adi.pb from keychain?"),
|
||||
primaryButton: .default(Text("do it")) {
|
||||
#if !DEBUG
|
||||
if Keychain.shared.adiPb != nil {
|
||||
Keychain.shared.adiPb = nil
|
||||
}
|
||||
#endif
|
||||
print("Cleared adi.pb from keychain")
|
||||
errorCallback()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
},
|
||||
secondaryButton: .cancel(Text("cancel")) {
|
||||
print("canceled")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.navigationTitle("")
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// ErrorDetailsViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/5/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class ErrorDetailsViewController: UIViewController
|
||||
{
|
||||
var loggedError: LoggedError?
|
||||
|
||||
@IBOutlet private var textView: UITextView!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
if let error = self.loggedError?.error
|
||||
{
|
||||
self.title = error.localizedErrorCode
|
||||
|
||||
let font = self.textView.font ?? UIFont.preferredFont(forTextStyle: .body)
|
||||
let detailedDescription = error.formattedDetailedDescription(with: font)
|
||||
self.textView.attributedText = detailedDescription
|
||||
}
|
||||
else
|
||||
{
|
||||
self.title = NSLocalizedString("Error Details", comment: "")
|
||||
}
|
||||
|
||||
self.navigationController?.navigationBar.tintColor = .altPrimary
|
||||
|
||||
if #available(iOS 15, *), let sheetController = self.navigationController?.sheetPresentationController
|
||||
{
|
||||
sheetController.detents = [.medium(), .large()]
|
||||
sheetController.selectedDetentIdentifier = .medium
|
||||
sheetController.prefersGrabberVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
self.textView.textContainerInset.left = self.view.layoutMargins.left
|
||||
self.textView.textContainerInset.right = self.view.layoutMargins.right
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
//
|
||||
// InsetGroupTableViewCell.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension InsetGroupTableViewCell
|
||||
{
|
||||
@objc enum Style: Int
|
||||
{
|
||||
case single
|
||||
case top
|
||||
case middle
|
||||
case bottom
|
||||
}
|
||||
}
|
||||
|
||||
final class InsetGroupTableViewCell: UITableViewCell
|
||||
{
|
||||
#if !TARGET_INTERFACE_BUILDER
|
||||
@IBInspectable var style: Style = .single {
|
||||
didSet {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
#else
|
||||
@IBInspectable var style: Int = 0
|
||||
#endif
|
||||
|
||||
@IBInspectable var isSelectable: Bool = false
|
||||
|
||||
private let separatorView = UIView()
|
||||
private let insetView = UIView()
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.selectionStyle = .none
|
||||
|
||||
self.separatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.separatorView.backgroundColor = UIColor.white.withAlphaComponent(0.25)
|
||||
self.addSubview(self.separatorView)
|
||||
|
||||
self.insetView.layer.masksToBounds = true
|
||||
self.insetView.layer.cornerRadius = 16
|
||||
|
||||
// Get the preferred background color from Interface Builder.
|
||||
self.insetView.backgroundColor = self.backgroundColor
|
||||
self.backgroundColor = nil
|
||||
|
||||
self.addSubview(self.insetView, pinningEdgesWith: UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15))
|
||||
self.sendSubviewToBack(self.insetView)
|
||||
|
||||
NSLayoutConstraint.activate([self.separatorView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 30),
|
||||
self.separatorView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -30),
|
||||
self.separatorView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
self.separatorView.heightAnchor.constraint(equalToConstant: 1)])
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool)
|
||||
{
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
if animated
|
||||
{
|
||||
UIView.animate(withDuration: 0.4) {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, animated: Bool)
|
||||
{
|
||||
super.setHighlighted(highlighted, animated: animated)
|
||||
|
||||
if animated
|
||||
{
|
||||
UIView.animate(withDuration: 0.4) {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension InsetGroupTableViewCell
|
||||
{
|
||||
func update()
|
||||
{
|
||||
switch self.style
|
||||
{
|
||||
case .single:
|
||||
self.insetView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
self.separatorView.isHidden = true
|
||||
|
||||
case .top:
|
||||
self.insetView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
self.separatorView.isHidden = false
|
||||
|
||||
case .middle:
|
||||
self.insetView.layer.maskedCorners = []
|
||||
self.separatorView.isHidden = false
|
||||
|
||||
case .bottom:
|
||||
self.insetView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
self.separatorView.isHidden = true
|
||||
}
|
||||
|
||||
if self.isSelectable && (self.isHighlighted || self.isSelected)
|
||||
{
|
||||
self.insetView.backgroundColor = UIColor.white.withAlphaComponent(0.55)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.insetView.backgroundColor = UIColor.white.withAlphaComponent(0.25)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// LicensesViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/6/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class LicensesViewController: UIViewController
|
||||
{
|
||||
private var _didAppear = false
|
||||
|
||||
@IBOutlet private var textView: UITextView!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.view.setNeedsLayout()
|
||||
self.view.layoutIfNeeded()
|
||||
|
||||
// Fix incorrect initial offset on iPhone SE.
|
||||
self.textView.contentOffset.y = 0
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
_didAppear = true
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
self.textView.textContainerInset.left = self.view.layoutMargins.left
|
||||
self.textView.textContainerInset.right = self.view.layoutMargins.right
|
||||
self.textView.textContainer.lineFragmentPadding = 0
|
||||
|
||||
if !_didAppear
|
||||
{
|
||||
// Fix incorrect initial offset on iPhone SE.
|
||||
self.textView.contentOffset.y = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
//
|
||||
// PatreonComponents.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class PatronCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
}
|
||||
|
||||
final class PatronsHeaderView: UICollectionReusableView
|
||||
{
|
||||
let textLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
self.textLabel.font = UIFont.boldSystemFont(ofSize: 17)
|
||||
self.textLabel.textColor = .white
|
||||
self.addSubview(self.textLabel, pinningEdgesWith: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20))
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
final class PatronsFooterView: UICollectionReusableView
|
||||
{
|
||||
let button = UIButton(type: .system)
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
self.button.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.button.activityIndicatorView.style = .medium
|
||||
self.button.titleLabel?.textColor = .white
|
||||
self.addSubview(self.button)
|
||||
|
||||
NSLayoutConstraint.activate([self.button.centerXAnchor.constraint(equalTo: self.centerXAnchor),
|
||||
self.button.centerYAnchor.constraint(equalTo: self.centerYAnchor)])
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
final class AboutPatreonHeaderView: UICollectionReusableView
|
||||
{
|
||||
@IBOutlet var supportButton: UIButton!
|
||||
@IBOutlet var twitterButton: UIButton!
|
||||
@IBOutlet var instagramButton: UIButton!
|
||||
@IBOutlet var accountButton: UIButton!
|
||||
@IBOutlet var textView: UITextView!
|
||||
|
||||
@IBOutlet private var rileyLabel: UILabel!
|
||||
@IBOutlet private var shaneLabel: UILabel!
|
||||
|
||||
@IBOutlet private var rileyImageView: UIImageView!
|
||||
@IBOutlet private var shaneImageView: UIImageView!
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.textView.clipsToBounds = true
|
||||
self.textView.layer.cornerRadius = 20
|
||||
self.textView.textContainer.lineFragmentPadding = 0
|
||||
|
||||
for imageView in [self.rileyImageView, self.shaneImageView].compactMap({ $0 })
|
||||
{
|
||||
imageView.clipsToBounds = true
|
||||
imageView.layer.cornerRadius = imageView.bounds.midY
|
||||
}
|
||||
|
||||
for button in [self.supportButton, self.accountButton, self.twitterButton, self.instagramButton].compactMap({ $0 })
|
||||
{
|
||||
button.clipsToBounds = true
|
||||
button.layer.cornerRadius = 16
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutMarginsDidChange()
|
||||
{
|
||||
super.layoutMarginsDidChange()
|
||||
|
||||
self.textView.textContainerInset = UIEdgeInsets(top: self.layoutMargins.left, left: self.layoutMargins.left, bottom: self.layoutMargins.right, right: self.layoutMargins.right)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// SettingsHeaderFooterView.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
final class SettingsHeaderFooterView: UITableViewHeaderFooterView
|
||||
{
|
||||
@IBOutlet var primaryLabel: UILabel!
|
||||
@IBOutlet var secondaryLabel: UILabel!
|
||||
@IBOutlet var button: UIButton!
|
||||
|
||||
@IBOutlet private var stackView: UIStackView!
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.contentView.layoutMargins = .zero
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.contentView.addSubview(self.stackView)
|
||||
|
||||
NSLayoutConstraint.activate([self.stackView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor),
|
||||
self.stackView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor),
|
||||
self.stackView.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor),
|
||||
self.stackView.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor)])
|
||||
}
|
||||
}
|
||||
@@ -1,846 +0,0 @@
|
||||
//
|
||||
// SettingsViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import SafariServices
|
||||
import MessageUI
|
||||
import Intents
|
||||
import IntentsUI
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension SettingsViewController
|
||||
{
|
||||
fileprivate enum Section: Int, CaseIterable
|
||||
{
|
||||
case signIn
|
||||
case account
|
||||
case patreon
|
||||
case appRefresh
|
||||
case instructions
|
||||
case credits
|
||||
case debug
|
||||
}
|
||||
|
||||
fileprivate enum AppRefreshRow: Int, CaseIterable
|
||||
{
|
||||
case backgroundRefresh
|
||||
case noIdleTimeout
|
||||
case disableAppLimit
|
||||
|
||||
@available(iOS 14, *)
|
||||
case addToSiri
|
||||
|
||||
static var allCases: [AppRefreshRow] {
|
||||
var c: [AppRefreshRow] = [.backgroundRefresh, .noIdleTimeout]
|
||||
guard #available(iOS 14, *) else { return c }
|
||||
if !ProcessInfo().sparseRestorePatched { c.append(.disableAppLimit) }
|
||||
c.append(.addToSiri)
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum CreditsRow: Int, CaseIterable
|
||||
{
|
||||
case developer
|
||||
case operations
|
||||
case designer
|
||||
case softwareLicenses
|
||||
}
|
||||
|
||||
fileprivate enum DebugRow: Int, CaseIterable
|
||||
{
|
||||
case sendFeedback
|
||||
case refreshAttempts
|
||||
case errorLog
|
||||
case refreshSideJITServer
|
||||
case clearCache
|
||||
case resetPairingFile
|
||||
case anisetteServers
|
||||
case advancedSettings
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
final class SettingsViewController: UITableViewController
|
||||
{
|
||||
private var activeTeam: Team?
|
||||
|
||||
private var prototypeHeaderFooterView: SettingsHeaderFooterView!
|
||||
|
||||
private var debugGestureCounter = 0
|
||||
private weak var debugGestureTimer: Timer?
|
||||
|
||||
@IBOutlet private var accountNameLabel: UILabel!
|
||||
@IBOutlet private var accountEmailLabel: UILabel!
|
||||
@IBOutlet private var accountTypeLabel: UILabel!
|
||||
|
||||
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
|
||||
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
|
||||
@IBOutlet private var disableAppLimitSwitch: UISwitch!
|
||||
|
||||
@IBOutlet private var refreshSideJITServer: UILabel!
|
||||
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
let nib = UINib(nibName: "SettingsHeaderFooterView", bundle: nil)
|
||||
self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView
|
||||
|
||||
self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "HeaderFooterView")
|
||||
|
||||
let debugModeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(SettingsViewController.handleDebugModeGesture(_:)))
|
||||
debugModeGestureRecognizer.delegate = self
|
||||
debugModeGestureRecognizer.direction = .up
|
||||
debugModeGestureRecognizer.numberOfTouchesRequired = 3
|
||||
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
|
||||
|
||||
var versionString: String = ""
|
||||
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
||||
{
|
||||
versionString += "SideStore \(version)"
|
||||
if let xcode = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String {
|
||||
versionString += " - Xcode \(xcode) - "
|
||||
if let build = Bundle.main.object(forInfoDictionaryKey: "DTXcodeBuild") as? String {
|
||||
versionString += "\(build)"
|
||||
}
|
||||
}
|
||||
if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String {
|
||||
let pair_test = pairing == "<insert pairing file here>"
|
||||
if !pair_test {
|
||||
versionString += " - \(!pair_test)"
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
versionString += "SideStore\t"
|
||||
}
|
||||
versionString += "\n\(Bundle.Info.appbundleIdentifier)"
|
||||
|
||||
self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version")
|
||||
|
||||
self.versionLabel.numberOfLines = 0
|
||||
self.versionLabel.lineBreakMode = .byWordWrapping
|
||||
self.versionLabel.setNeedsUpdateConstraints()
|
||||
|
||||
self.tableView.contentInset.bottom = 40
|
||||
|
||||
self.update()
|
||||
|
||||
if #available(iOS 15, *), let appearance = self.tabBarController?.tabBar.standardAppearance
|
||||
{
|
||||
appearance.stackedLayoutAppearance.normal.badgeBackgroundColor = .altPrimary
|
||||
self.navigationController?.tabBarItem.scrollEdgeAppearance = appearance
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if segue.identifier == "anisetteServers" {
|
||||
let controller = UIHostingController(rootView: AnisetteServers(selected: UserDefaults.standard.menuAnisetteURL, errorCallback: {
|
||||
ToastView(text: "Cleared adi.pb!", detailText: "You will need to log back into Apple ID in SideStore.").show(in: self)
|
||||
}))
|
||||
self.show(controller, sender: nil)
|
||||
} else {
|
||||
super.prepare(for: segue, sender: sender)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension SettingsViewController
|
||||
{
|
||||
func update()
|
||||
{
|
||||
if let team = DatabaseManager.shared.activeTeam()
|
||||
{
|
||||
self.accountNameLabel.text = team.name
|
||||
self.accountEmailLabel.text = team.account.appleID
|
||||
self.accountTypeLabel.text = team.type.localizedDescription
|
||||
|
||||
self.activeTeam = team
|
||||
}
|
||||
else
|
||||
{
|
||||
self.activeTeam = nil
|
||||
}
|
||||
|
||||
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
|
||||
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
|
||||
|
||||
if self.isViewLoaded
|
||||
{
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
func prepare(_ settingsHeaderFooterView: SettingsHeaderFooterView, for section: Section, isHeader: Bool)
|
||||
{
|
||||
settingsHeaderFooterView.primaryLabel.isHidden = !isHeader
|
||||
settingsHeaderFooterView.secondaryLabel.isHidden = isHeader
|
||||
settingsHeaderFooterView.button.isHidden = true
|
||||
|
||||
settingsHeaderFooterView.layoutMargins.bottom = isHeader ? 0 : 8
|
||||
|
||||
switch section
|
||||
{
|
||||
case .signIn:
|
||||
if isHeader
|
||||
{
|
||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ACCOUNT", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Sign in with your Apple ID to download apps from SideStore.", comment: "")
|
||||
}
|
||||
|
||||
case .patreon:
|
||||
if isHeader
|
||||
{
|
||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("SUPPORT US", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Support the SideStore Team by following our socials or becoming a patron!", comment: "")
|
||||
}
|
||||
|
||||
case .account:
|
||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ACCOUNT", comment: "")
|
||||
|
||||
settingsHeaderFooterView.button.setTitle(NSLocalizedString("SIGN OUT", comment: ""), for: .normal)
|
||||
settingsHeaderFooterView.button.addTarget(self, action: #selector(SettingsViewController.signOut(_:)), for: .primaryActionTriggered)
|
||||
settingsHeaderFooterView.button.isHidden = false
|
||||
|
||||
case .appRefresh:
|
||||
if isHeader
|
||||
{
|
||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("REFRESHING APPS", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Enable Background Refresh to automatically refresh apps in the background when connected to Wi-Fi. \n\nEnable Disable Idle Timeout to allow SideStore to keep your device awake during a refresh or install of any apps.", comment: "")
|
||||
}
|
||||
|
||||
case .instructions:
|
||||
break
|
||||
|
||||
case .credits:
|
||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
|
||||
|
||||
case .debug:
|
||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DEBUG", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
func preferredHeight(for settingsHeaderFooterView: SettingsHeaderFooterView, in section: Section, isHeader: Bool) -> CGFloat
|
||||
{
|
||||
let widthConstraint = settingsHeaderFooterView.contentView.widthAnchor.constraint(equalToConstant: tableView.bounds.width)
|
||||
NSLayoutConstraint.activate([widthConstraint])
|
||||
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
|
||||
|
||||
self.prepare(settingsHeaderFooterView, for: section, isHeader: isHeader)
|
||||
|
||||
let size = settingsHeaderFooterView.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
return size.height
|
||||
}
|
||||
}
|
||||
|
||||
private extension SettingsViewController
|
||||
{
|
||||
func signIn()
|
||||
{
|
||||
AppManager.shared.authenticate(presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(OperationError.cancelled):
|
||||
// Ignore
|
||||
break
|
||||
|
||||
case .failure(let error):
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
|
||||
case .success: break
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func signOut(_ sender: UIBarButtonItem)
|
||||
{
|
||||
func signOut()
|
||||
{
|
||||
DatabaseManager.shared.signOut { (error) in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error
|
||||
{
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to sign out?", comment: ""), message: NSLocalizedString("You will no longer be able to install or refresh apps once you sign out.", comment: ""), preferredStyle: .actionSheet)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Sign Out", comment: ""), style: .destructive) { _ in signOut() })
|
||||
alertController.addAction(.cancel)
|
||||
//Fix crash on iPad
|
||||
alertController.popoverPresentationController?.barButtonItem = sender
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func toggleDisableAppLimit(_ sender: UISwitch) {
|
||||
UserDefaults.standard.isAppLimitDisabled = sender.isOn
|
||||
}
|
||||
|
||||
@IBAction func toggleIsBackgroundRefreshEnabled(_ sender: UISwitch)
|
||||
{
|
||||
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn
|
||||
}
|
||||
|
||||
@IBAction func toggleNoIdleTimeoutEnabled(_ sender: UISwitch)
|
||||
{
|
||||
UserDefaults.standard.isIdleTimeoutDisableEnabled = sender.isOn
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
@IBAction func addRefreshAppsShortcut()
|
||||
{
|
||||
guard let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) else { return }
|
||||
|
||||
let viewController = INUIAddVoiceShortcutViewController(shortcut: shortcut)
|
||||
viewController.delegate = self
|
||||
viewController.modalPresentationStyle = .formSheet
|
||||
self.present(viewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func clearCache()
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear SideStore's cache?", comment: ""),
|
||||
message: NSLocalizedString("This will remove all temporary files as well as backups for uninstalled apps.", comment: ""),
|
||||
preferredStyle: .actionSheet)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { [weak self] _ in
|
||||
self?.tableView.indexPathForSelectedRow.map { self?.tableView.deselectRow(at: $0, animated: true) }
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Clear Cache", comment: ""), style: .destructive) { [weak self] _ in
|
||||
AppManager.shared.clearAppCache { result in
|
||||
DispatchQueue.main.async {
|
||||
self?.tableView.indexPathForSelectedRow.map { self?.tableView.deselectRow(at: $0, animated: true) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error):
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Unable to Clear Cache", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self?.present(alertController, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if let popoverController = alertController.popoverPresentationController {
|
||||
popoverController.sourceView = self.view
|
||||
popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
|
||||
}
|
||||
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
@IBAction func handleDebugModeGesture(_ gestureRecognizer: UISwipeGestureRecognizer)
|
||||
{
|
||||
self.debugGestureCounter += 1
|
||||
self.debugGestureTimer?.invalidate()
|
||||
|
||||
if self.debugGestureCounter >= 3
|
||||
{
|
||||
self.debugGestureCounter = 0
|
||||
|
||||
UserDefaults.standard.isDebugModeEnabled.toggle()
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
else
|
||||
{
|
||||
self.debugGestureTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false) { [weak self] (timer) in
|
||||
self?.debugGestureCounter = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func openTwitter(username: String)
|
||||
{
|
||||
let twitterAppURL = URL(string: "twitter://user?screen_name=" + username)!
|
||||
UIApplication.shared.open(twitterAppURL, options: [:]) { (success) in
|
||||
if success
|
||||
{
|
||||
if let selectedIndexPath = self.tableView.indexPathForSelectedRow
|
||||
{
|
||||
self.tableView.deselectRow(at: selectedIndexPath, animated: true)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let safariURL = URL(string: "https://twitter.com/" + username)!
|
||||
|
||||
let safariViewController = SFSafariViewController(url: safariURL)
|
||||
safariViewController.preferredControlTintColor = .altPrimary
|
||||
self.present(safariViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SettingsViewController
|
||||
{
|
||||
@objc func openPatreonSettings(_ notification: Notification)
|
||||
{
|
||||
guard self.presentedViewController == nil else { return }
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
self.navigationController?.popViewController(animated: false)
|
||||
self.performSegue(withIdentifier: "showPatreon", sender: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func openErrorLog(_: Notification) {
|
||||
guard self.presentedViewController == nil else { return }
|
||||
|
||||
self.navigationController?.popViewController(animated: false)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.performSegue(withIdentifier: "showErrorLog", sender: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController
|
||||
{
|
||||
override func numberOfSections(in tableView: UITableView) -> Int
|
||||
{
|
||||
var numberOfSections = super.numberOfSections(in: tableView)
|
||||
|
||||
if !UserDefaults.standard.isDebugModeEnabled
|
||||
{
|
||||
numberOfSections -= 1
|
||||
}
|
||||
|
||||
return numberOfSections
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
|
||||
{
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case .signIn: return (self.activeTeam == nil) ? 1 : 0
|
||||
case .account: return (self.activeTeam == nil) ? 0 : 3
|
||||
case .appRefresh: return AppRefreshRow.allCases.count
|
||||
default: return super.tableView(tableView, numberOfRowsInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
|
||||
{
|
||||
let cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||
|
||||
if #available(iOS 14, *) {}
|
||||
else if let cell = cell as? InsetGroupTableViewCell,
|
||||
indexPath.section == Section.appRefresh.rawValue,
|
||||
indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
|
||||
{
|
||||
// Only one row is visible pre-iOS 14.
|
||||
cell.style = .single
|
||||
}
|
||||
|
||||
if AppRefreshRow.AllCases().count == 1
|
||||
{
|
||||
if let cell = cell as? InsetGroupTableViewCell,
|
||||
indexPath.section == Section.appRefresh.rawValue,
|
||||
indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
|
||||
{
|
||||
cell.style = .single
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
|
||||
{
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case .signIn where self.activeTeam != nil: return nil
|
||||
case .account where self.activeTeam == nil: return nil
|
||||
case .signIn, .account, .patreon, .appRefresh, .credits, .debug:
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
|
||||
self.prepare(headerView, for: section, isHeader: true)
|
||||
return headerView
|
||||
|
||||
case .instructions: return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
|
||||
{
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case .signIn where self.activeTeam != nil: return nil
|
||||
case .signIn, .patreon, .appRefresh:
|
||||
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
|
||||
self.prepare(footerView, for: section, isHeader: false)
|
||||
return footerView
|
||||
|
||||
case .account, .credits, .debug, .instructions: return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
|
||||
{
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case .signIn where self.activeTeam != nil: return 1.0
|
||||
case .account where self.activeTeam == nil: return 1.0
|
||||
case .signIn, .account, .patreon, .appRefresh, .credits, .debug:
|
||||
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
|
||||
return height
|
||||
|
||||
case .instructions: return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
|
||||
{
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case .signIn where self.activeTeam != nil: return 1.0
|
||||
case .account where self.activeTeam == nil: return 1.0
|
||||
case .signIn, .patreon, .appRefresh:
|
||||
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
|
||||
return height
|
||||
|
||||
case .account, .credits, .debug, .instructions: return 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
let section = Section.allCases[indexPath.section]
|
||||
switch section
|
||||
{
|
||||
case .signIn: self.signIn()
|
||||
case .instructions: break
|
||||
case .appRefresh:
|
||||
let row = AppRefreshRow.allCases[indexPath.row]
|
||||
switch row
|
||||
{
|
||||
case .backgroundRefresh: break
|
||||
case .noIdleTimeout: break
|
||||
case .disableAppLimit: break
|
||||
case .addToSiri:
|
||||
guard #available(iOS 14, *) else { return }
|
||||
self.addRefreshAppsShortcut()
|
||||
}
|
||||
|
||||
|
||||
case .credits:
|
||||
let row = CreditsRow.allCases[indexPath.row]
|
||||
switch row
|
||||
{
|
||||
case .developer: self.openTwitter(username: "sidestore_io")
|
||||
case .operations: self.openTwitter(username: "sidestore_io")
|
||||
case .designer: self.openTwitter(username: "lit_ritt")
|
||||
case .softwareLicenses: break
|
||||
}
|
||||
|
||||
case .debug:
|
||||
let row = DebugRow.allCases[indexPath.row]
|
||||
switch row
|
||||
{
|
||||
case .sendFeedback:
|
||||
let alertController = UIAlertController(title: "Send Feedback", message: "Choose a method to send feedback:", preferredStyle: .actionSheet)
|
||||
|
||||
// Option 1: GitHub
|
||||
alertController.addAction(UIAlertAction(title: "GitHub", style: .default) { _ in
|
||||
if let githubURL = URL(string: "https://github.com/SideStore/SideStore/issues") {
|
||||
let safariViewController = SFSafariViewController(url: githubURL)
|
||||
safariViewController.preferredControlTintColor = .altPrimary
|
||||
self.present(safariViewController, animated: true, completion: nil)
|
||||
}
|
||||
})
|
||||
|
||||
// Option 2: Discord
|
||||
alertController.addAction(UIAlertAction(title: "Discord", style: .default) { _ in
|
||||
if let discordURL = URL(string: "https://discord.gg/sidestore-949183273383395328") {
|
||||
let safariViewController = SFSafariViewController(url: discordURL)
|
||||
safariViewController.preferredControlTintColor = .altPrimary
|
||||
self.present(safariViewController, animated: true, completion: nil)
|
||||
}
|
||||
})
|
||||
|
||||
// Option 3: Mail
|
||||
// alertController.addAction(UIAlertAction(title: "Send Email", style: .default) { _ in
|
||||
// if MFMailComposeViewController.canSendMail() {
|
||||
// let mailViewController = MFMailComposeViewController()
|
||||
// mailViewController.mailComposeDelegate = self
|
||||
// mailViewController.setToRecipients(["support@sidestore.io"])
|
||||
//
|
||||
// if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
|
||||
// mailViewController.setSubject("SideStore Beta \(version) Feedback")
|
||||
// } else {
|
||||
// mailViewController.setSubject("SideStore Beta Feedback")
|
||||
// }
|
||||
//
|
||||
// self.present(mailViewController, animated: true, completion: nil)
|
||||
// } else {
|
||||
// let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil)
|
||||
// toastView.show(in: self)
|
||||
// }
|
||||
// })
|
||||
|
||||
// Cancel action
|
||||
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
|
||||
// For iPad: Set the source view if presenting on iPad to avoid crashes
|
||||
if let popoverController = alertController.popoverPresentationController {
|
||||
popoverController.sourceView = self.view
|
||||
popoverController.sourceRect = self.view.bounds
|
||||
}
|
||||
|
||||
// Present the action sheet
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
|
||||
case .refreshSideJITServer:
|
||||
if #available(iOS 17, *) {
|
||||
|
||||
let alertController = UIAlertController(
|
||||
title: NSLocalizedString("SideJITServer", comment: ""),
|
||||
message: NSLocalizedString("Settings for SideJITServer", comment: ""),
|
||||
preferredStyle: UIAlertController.Style.actionSheet)
|
||||
|
||||
|
||||
if UserDefaults.standard.sidejitenable {
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Disable", comment: ""), style: .default){ _ in
|
||||
UserDefaults.standard.sidejitenable = false
|
||||
})
|
||||
} else {
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Enable", comment: ""), style: .default){ _ in
|
||||
UserDefaults.standard.sidejitenable = true
|
||||
})
|
||||
}
|
||||
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Server Address", comment: ""), style: .default){ _ in
|
||||
let alertController1 = UIAlertController(title: "SideJITServer Address", message: "Please Enter the SideJITServer Address Below. (this is not needed if SideJITServer has already been detected)", preferredStyle: .alert)
|
||||
|
||||
|
||||
alertController1.addTextField { textField in
|
||||
textField.placeholder = "SideJITServer Address"
|
||||
}
|
||||
|
||||
|
||||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
|
||||
alertController1.addAction(cancelAction)
|
||||
|
||||
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
||||
if let text = alertController1.textFields?.first?.text {
|
||||
UserDefaults.standard.textInputSideJITServerurl = text
|
||||
}
|
||||
}
|
||||
|
||||
alertController1.addAction(okAction)
|
||||
|
||||
// Present the alert controller
|
||||
self.present(alertController1, animated: true)
|
||||
})
|
||||
|
||||
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh", comment: ""), style: .destructive){ _ in
|
||||
if UserDefaults.standard.sidejitenable {
|
||||
var SJSURL = ""
|
||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||
} else {
|
||||
SJSURL = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||
}
|
||||
|
||||
|
||||
let url = URL(string: SJSURL + "/re/")!
|
||||
|
||||
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
||||
if let error = error {
|
||||
print("Error: \(error)")
|
||||
} else {
|
||||
// Do nothing with data or response
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
//Fix crash on iPad
|
||||
alertController.popoverPresentationController?.sourceView = self.tableView
|
||||
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
|
||||
self.present(alertController, animated: true)
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
} else {
|
||||
let alertController = UIAlertController(
|
||||
title: NSLocalizedString("You are not on iOS 17+ This will not work", comment: ""),
|
||||
message: NSLocalizedString("This is meant for 'SideJITServer' and it only works on iOS 17+ ", comment: ""),
|
||||
preferredStyle: UIAlertController.Style.actionSheet)
|
||||
|
||||
alertController.addAction(.cancel)
|
||||
//Fix crash on iPad
|
||||
alertController.popoverPresentationController?.sourceView = self.tableView
|
||||
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
|
||||
self.present(alertController, animated: true)
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
|
||||
case .clearCache: self.clearCache()
|
||||
|
||||
case .resetPairingFile:
|
||||
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
|
||||
let fm = FileManager.default
|
||||
|
||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||
let alertController = UIAlertController(
|
||||
title: NSLocalizedString("Are you sure to reset the pairing file?", comment: ""),
|
||||
message: NSLocalizedString("You can reset the pairing file when you cannot sideload apps or enable JIT. You need to restart SideStore.", comment: ""),
|
||||
preferredStyle: UIAlertController.Style.actionSheet)
|
||||
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete and Reset", comment: ""), style: .destructive){ _ in
|
||||
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
|
||||
UserDefaults.standard.isPairingReset = true
|
||||
try? fm.removeItem(atPath: documentsPath.path)
|
||||
NSLog("Pairing File Reseted")
|
||||
}
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
let dialogMessage = UIAlertController(title: NSLocalizedString("Pairing File Reset", comment: ""), message: NSLocalizedString("Please restart SideStore", comment: ""), preferredStyle: .alert)
|
||||
self.present(dialogMessage, animated: true, completion: nil)
|
||||
})
|
||||
alertController.addAction(.cancel)
|
||||
//Fix crash on iPad
|
||||
alertController.popoverPresentationController?.sourceView = self.tableView
|
||||
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
|
||||
self.present(alertController, animated: true)
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
case .anisetteServers:
|
||||
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: UIHostingController(rootView: AnisetteServers(selected: "", errorCallback: {
|
||||
ToastView(text: "Reset adi.pb", detailText: "Buh").show(in: self)
|
||||
}))), sender: nil)
|
||||
// self.performSegue(withIdentifier: "anisetteServers", sender: nil)
|
||||
case .advancedSettings:
|
||||
// Create the URL that deep links to your app's custom settings.
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
// Ask the system to open that URL.
|
||||
UIApplication.shared.open(url)
|
||||
} else {
|
||||
ELOG("UIApplication.openSettingsURLString invalid")
|
||||
}
|
||||
case .refreshAttempts, .errorLog: break
|
||||
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: MFMailComposeViewControllerDelegate
|
||||
{
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?)
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
|
||||
controller.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: UIGestureRecognizerDelegate
|
||||
{
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: INUIAddVoiceShortcutViewControllerDelegate
|
||||
{
|
||||
func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?)
|
||||
{
|
||||
if let indexPath = self.tableView.indexPathForSelectedRow
|
||||
{
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
controller.dismiss(animated: true, completion: nil)
|
||||
|
||||
guard let error = error else { return }
|
||||
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
|
||||
func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController)
|
||||
{
|
||||
if let indexPath = self.tableView.indexPathForSelectedRow
|
||||
{
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
controller.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
//
|
||||
// Managed.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/5/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@propertyWrapper @dynamicMemberLookup
|
||||
struct Managed<ManagedObject>
|
||||
{
|
||||
var wrappedValue: ManagedObject {
|
||||
didSet {
|
||||
self.managedObjectContext = self.managedObject?.managedObjectContext
|
||||
}
|
||||
}
|
||||
|
||||
var projectedValue: Managed<ManagedObject> {
|
||||
return self
|
||||
}
|
||||
|
||||
private var managedObjectContext: NSManagedObjectContext?
|
||||
private var managedObject: NSManagedObject? {
|
||||
return self.wrappedValue as? NSManagedObject
|
||||
}
|
||||
|
||||
init(wrappedValue: ManagedObject)
|
||||
{
|
||||
self.wrappedValue = wrappedValue
|
||||
self.managedObjectContext = self.managedObject?.managedObjectContext
|
||||
}
|
||||
|
||||
subscript<T>(dynamicMember keyPath: KeyPath<ManagedObject, T>) -> T
|
||||
{
|
||||
var result: T!
|
||||
|
||||
if let context = self.managedObjectContext
|
||||
{
|
||||
context.performAndWait {
|
||||
result = self.wrappedValue[keyPath: keyPath]
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = self.wrappedValue[keyPath: keyPath]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Optionals
|
||||
subscript<Wrapped, T>(dynamicMember keyPath: KeyPath<Wrapped, T>) -> T? where ManagedObject == Optional<Wrapped> {
|
||||
var result: T?
|
||||
|
||||
if let context = self.managedObjectContext {
|
||||
context.performAndWait {
|
||||
result = self.wrappedValue?[keyPath: keyPath] as? T
|
||||
}
|
||||
} else {
|
||||
result = self.wrappedValue?[keyPath: keyPath] as? T
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltStoreCore
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// AltStoreCore.h
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 9/3/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for AltStoreCore.
|
||||
FOUNDATION_EXPORT double AltStoreCoreVersionNumber;
|
||||
|
||||
//! Project version string for AltStoreCore.
|
||||
FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <AltStoreCore/PublicHeader.h>
|
||||
|
||||
#import <AltStoreCore/ALTAppPermission.h>
|
||||
#import <AltStoreCore/ALTSourceUserInfoKey.h>
|
||||
#import <AltStoreCore/ALTPatreonBenefitType.h>
|
||||
|
||||
// Shared
|
||||
#import <AltStoreCore/ALTConstants.h>
|
||||
#import <AltStoreCore/ALTConnection.h>
|
||||
#import <AltStoreCore/ALTWrappedError.h>
|
||||
#import <AltStoreCore/NSError+ALTServerError.h>
|
||||
#import <AltStoreCore/CFNotificationName+AltStore.h>
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// FileManager+SharedDirectories.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/14/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension FileManager
|
||||
{
|
||||
var altstoreSharedDirectory: URL? {
|
||||
guard let appGroup = Bundle.main.appGroups.first else { return nil }
|
||||
|
||||
let sharedDirectoryURL = self.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
||||
return sharedDirectoryURL
|
||||
}
|
||||
|
||||
var appBackupsDirectory: URL? {
|
||||
let appBackupsDirectory = self.altstoreSharedDirectory?.appendingPathComponent("Backups", isDirectory: true)
|
||||
return appBackupsDirectory
|
||||
}
|
||||
|
||||
func backupDirectoryURL(for app: InstalledApp) -> URL?
|
||||
{
|
||||
let backupDirectoryURL = self.appBackupsDirectory?.appendingPathComponent(app.bundleIdentifier, isDirectory: true)
|
||||
return backupDirectoryURL
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// OperatingSystemVersion+Comparable.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by nythepegasus on 5/9/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension OperatingSystemVersion: Comparable {
|
||||
public static func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
|
||||
return lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion
|
||||
}
|
||||
|
||||
public static func <(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
|
||||
return lhs.stringValue.compare(rhs.stringValue, options: .numeric) == .orderedAscending
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// String+SideStore.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by nythepegasus on 5/9/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
init(formatted: String, comment: String? = nil, _ args: String...) {
|
||||
self.init(format: NSLocalizedString(formatted, comment: comment ?? ""), args)
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//
|
||||
// UIColor+Hex.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/15/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension UIColor
|
||||
{
|
||||
// Borrowed from https://stackoverflow.com/a/26341062
|
||||
var hexString: String {
|
||||
let components = self.cgColor.components
|
||||
let r: CGFloat = components?[0] ?? 0.0
|
||||
let g: CGFloat = components?[1] ?? 0.0
|
||||
let b: CGFloat = components?[2] ?? 0.0
|
||||
|
||||
let hexString = String.init(format: "%02lX%02lX%02lX", lroundf(Float(r * 255)), lroundf(Float(g * 255)), lroundf(Float(b * 255)))
|
||||
return hexString
|
||||
}
|
||||
|
||||
// Borrowed from https://stackoverflow.com/a/33397427
|
||||
convenience init?(hexString: String)
|
||||
{
|
||||
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int = UInt32()
|
||||
Scanner(string: hex).scanHexInt32(&int)
|
||||
let a, r, g, b: UInt32
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255)
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
//
|
||||
// InstalledExtension.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 1/7/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(InstalledExtension)
|
||||
public class InstalledExtension: NSManagedObject, InstalledAppProtocol
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var bundleIdentifier: String
|
||||
@NSManaged public var resignedBundleIdentifier: String
|
||||
@NSManaged public var version: String
|
||||
|
||||
@NSManaged public var refreshedDate: Date
|
||||
@NSManaged public var expirationDate: Date
|
||||
@NSManaged public var installedDate: Date
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public var parentApp: InstalledApp?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(resignedAppExtension: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: InstalledExtension.entity(), insertInto: context)
|
||||
|
||||
self.bundleIdentifier = originalBundleIdentifier
|
||||
|
||||
self.refreshedDate = Date()
|
||||
self.installedDate = Date()
|
||||
|
||||
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
||||
|
||||
self.update(resignedAppExtension: resignedAppExtension)
|
||||
}
|
||||
|
||||
public func update(resignedAppExtension: ALTApplication)
|
||||
{
|
||||
self.name = resignedAppExtension.name
|
||||
|
||||
self.resignedBundleIdentifier = resignedAppExtension.bundleIdentifier
|
||||
self.version = resignedAppExtension.version
|
||||
|
||||
if let provisioningProfile = resignedAppExtension.provisioningProfile
|
||||
{
|
||||
self.update(provisioningProfile: provisioningProfile)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(provisioningProfile: ALTProvisioningProfile)
|
||||
{
|
||||
self.refreshedDate = provisioningProfile.creationDate
|
||||
self.expirationDate = provisioningProfile.expirationDate
|
||||
}
|
||||
}
|
||||
|
||||
public extension InstalledExtension
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledExtension>
|
||||
{
|
||||
return NSFetchRequest<InstalledExtension>(entityName: "InstalledExtension")
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// Benefit.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct BenefitResponse: Decodable
|
||||
{
|
||||
var id: String
|
||||
}
|
||||
}
|
||||
|
||||
public struct Benefit: Hashable
|
||||
{
|
||||
public var type: ALTPatreonBenefitType
|
||||
|
||||
init(response: PatreonAPI.BenefitResponse)
|
||||
{
|
||||
self.type = ALTPatreonBenefitType(response.id)
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
//
|
||||
// Tier.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct TierResponse: Decodable
|
||||
{
|
||||
struct Attributes: Decodable
|
||||
{
|
||||
var title: String
|
||||
}
|
||||
|
||||
struct Relationships: Decodable
|
||||
{
|
||||
struct Benefits: Decodable
|
||||
{
|
||||
var data: [BenefitResponse]
|
||||
}
|
||||
|
||||
var benefits: Benefits
|
||||
}
|
||||
|
||||
var id: String
|
||||
var attributes: Attributes
|
||||
|
||||
var relationships: Relationships
|
||||
}
|
||||
}
|
||||
|
||||
public struct Tier
|
||||
{
|
||||
public var name: String
|
||||
public var identifier: String
|
||||
|
||||
public var benefits: [Benefit] = []
|
||||
|
||||
init(response: PatreonAPI.TierResponse)
|
||||
{
|
||||
self.name = response.attributes.title
|
||||
self.identifier = response.id
|
||||
self.benefits = response.relationships.benefits.data.map(Benefit.init(response:))
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// ALTAppPermission.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NSString *ALTAppPermissionType NS_TYPED_EXTENSIBLE_ENUM;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypePhotos;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeCamera;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeLocation;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeContacts;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeReminders;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeAppleMusic;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeMicrophone;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeSpeechRecognition;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeBluetooth;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeNetwork;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeCalendars;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeTouchID;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeFaceID;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeSiri;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeMotion;
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// ALTAppPermission.m
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTAppPermission.h"
|
||||
|
||||
ALTAppPermissionType const ALTAppPermissionTypePhotos = @"photos";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeCamera = @"camera";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeLocation = @"location";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeContacts = @"contacts";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeReminders = @"reminders";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeAppleMusic = @"music";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeMicrophone = @"microphone";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeSpeechRecognition = @"speech-recognition";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio = @"background-audio";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch = @"background-fetch";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeBluetooth = @"bluetooth";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeNetwork = @"network";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeCalendars = @"calendars";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeTouchID = @"touchid";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeFaceID = @"faceid";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeSiri = @"siri";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeMotion = @"motion";
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// ALTPatreonBenefitType.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/27/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NSString *ALTPatreonBenefitType NS_TYPED_EXTENSIBLE_ENUM;
|
||||
extern ALTPatreonBenefitType const ALTPatreonBenefitTypeBetaAccess;
|
||||
extern ALTPatreonBenefitType const ALTPatreonBenefitTypeCredits;
|
||||
@@ -1,12 +0,0 @@
|
||||
//
|
||||
// ALTPatreonBenefitType.m
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/27/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTPatreonBenefitType.h"
|
||||
|
||||
ALTPatreonBenefitType const ALTPatreonBenefitTypeBetaAccess = @"1186336";
|
||||
ALTPatreonBenefitType const ALTPatreonBenefitTypeCredits = @"1186340";
|
||||
@@ -1,12 +0,0 @@
|
||||
//
|
||||
// ALTSourceUserInfoKey.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 11/4/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NSString *ALTSourceUserInfoKey NS_TYPED_EXTENSIBLE_ENUM;
|
||||
extern ALTSourceUserInfoKey const ALTSourceUserInfoKeyPatreonAccessToken;
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// ALTSourceUserInfoKey.m
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 11/4/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTSourceUserInfoKey.h"
|
||||
|
||||
ALTSourceUserInfoKey const ALTSourceUserInfoKeyPatreonAccessToken = @"patreonAccessToken";
|
||||
BIN
AltWidget/Assets.xcassets/AltStore.imageset/1024.png
vendored
BIN
AltWidget/Assets.xcassets/AltStore.imageset/1024.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 846 KiB |
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<title>sidestore-logo-svg</title>
|
||||
<style>
|
||||
.s0 { fill: none }
|
||||
.s1 { fill: #bebbb4 }
|
||||
</style>
|
||||
<g id="Layer">
|
||||
<g id="Layer">
|
||||
<path id="Main" class="s0" />
|
||||
<g id="Main1">
|
||||
<g id="Layer">
|
||||
<path id="Layer" fill-rule="evenodd" class="s1" d="m86 50c0 2.3-1.9 4.2-4.2 4.2h-50.9c-2.3 0-4.2-1.9-4.2-4.2 0-2.3 1.9-4.2 4.2-4.2h50.9c2.3 0 4.2 1.9 4.2 4.2zm-31.8-23.3v2.1c0 3.4-1.3 6.6-3.7 9-2.4 2.4-5.6 3.7-9 3.7h-10.6c-2.2 0-4.4 0.9-5.9 2.5-1.6 1.6-2.5 3.8-2.5 6 0 2.2 0.9 4.4 2.5 6 1.5 1.6 3.7 2.5 5.9 2.5h10.6c3.4 0 6.6 1.3 9 3.7 2.4 2.4 3.7 5.6 3.7 9v10.8c0 1.1-0.4 2.1-1.1 2.8-0.8 0.8-1.8 1.2-2.9 1.2h-0.4c-1.1 0-2.1-0.4-2.9-1.2-0.7-0.7-1.1-1.7-1.1-2.8v-10.8c0-1.1-0.5-2.2-1.3-3-0.8-0.8-1.8-1.3-3-1.3h-10.6c-4.5 0-8.8-1.7-11.9-4.9-3.2-3.2-5-7.5-5-12 0-4.5 1.8-8.8 5-12 3.1-3.2 7.4-4.9 11.9-4.9h10.6c1.2 0 2.2-0.5 3-1.3 0.8-0.8 1.3-1.9 1.3-3v-2.1h-4.3l8.5-12.7 8.5 12.7zm12.7 55.3c0 2.2-1.8 4-4 4h-0.5c-2.2 0-4-1.8-4-4v-19.5c0-2.2 1.8-4 4-4h0.5c2.2 0 4 1.8 4 4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="100" height="100" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="50" fill="#fff" stroke-width=".265"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 175 B |
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// View+AltWidget.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/18/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View
|
||||
{
|
||||
@ViewBuilder
|
||||
func widgetBackground(_ backgroundView: some View) -> some View
|
||||
{
|
||||
if #available(iOSApplicationExtension 17, *)
|
||||
{
|
||||
containerBackground(for: .widget) {
|
||||
backgroundView
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
background(backgroundView)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>XYZ0123456.com.SideStore.SideStore.AltWidget</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>XYZ0123456</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
</array>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,3 +0,0 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltWidget
|
||||
@@ -1,27 +1,31 @@
|
||||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
MARKETING_VERSION = 0.5.9
|
||||
CURRENT_PROJECT_VERSION = 5090
|
||||
MARKETING_VERSION = 0.3.2
|
||||
CURRENT_PROJECT_VERSION = 3050
|
||||
|
||||
// Vars to be overwritten by `CodeSigning.xcconfig` if exists
|
||||
DEVELOPMENT_TEAM = S32Z3HMYVQ
|
||||
ORG_IDENTIFIER = com.SideStore
|
||||
ORG_IDENTIFIER = com.$(PROJECT_NAME)
|
||||
|
||||
// Codesigning settings defined optionally, see `CodeSigning.xcconfig.example`
|
||||
#include? "CodeSigning.xcconfig"
|
||||
|
||||
ORG_PREFIX = $(ORG_IDENTIFIER)
|
||||
|
||||
PRODUCT_NAME = SideStore
|
||||
//PRODUCT_NAME[configuration=Debug] = Prov Debug
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).SideStore
|
||||
// add team ID to bundle ID for debug builds since these will most likely be installed via Xcode
|
||||
// SideStore will expect the team ID to be at the end of the bundle ID, but this doesn't happen when we install via Xcode
|
||||
// we don't want to do this for release since those builds will most likely be installed via SideServer, which adds the team ID
|
||||
PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = $(ORG_PREFIX).SideStore.$(DEVELOPMENT_TEAM)
|
||||
|
||||
EXTENSION_PREFIX = $(PRODUCT_BUNDLE_IDENTIFIER)
|
||||
APP_GROUP_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)
|
||||
APP_GROUP_IDENTIFIER = $(ORG_PREFIX).$(PROJECT_NAME)
|
||||
ICLOUD_CONTAINER_IDENTIFIER = iCloud.$(ORG_PREFIX).$(PROJECT_NAME)
|
||||
|
||||
PRODUCT_NAME = $(PROJECT_NAME)
|
||||
PRODUCT_NAME[config=Debug] = $(PROJECT_NAME) Debug
|
||||
|
||||
EXTENSION_PREFIX = $(ORG_PREFIX).$(PROJECT_NAME)
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).$(PROJECT_NAME:lower)
|
||||
PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = $(ORG_PREFIX).$(PROJECT_NAME:lower)-debug
|
||||
|
||||
// MARK: - XCode / Swift
|
||||
|
||||
// Clang and the build system support a new mode for building module dependencies called explicit modules
|
||||
// which improves build performance, reliability, and correctness.
|
||||
// https://developer.apple.com/documentation/xcode-release-notes/xcode-14_3-release-notes#New-Features-in-Xcode-143-Beta-2
|
||||
_EXPERIMENTAL_CLANG_EXPLICIT_MODULES = YES
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
|
||||
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
Thank you for your interest in contributing to SideStore! SideStore is a community driven project, and it's made possible by people like you.
|
||||
|
||||
By contributing to this Project (SideStore), you agree to the Developer's Certificate of Origin found in [CERTIFICATE-OF-ORIGIN.md](CERTIFICATE-OF-ORIGIN.md). Any contributions to this project after the addition of the Developer's Certificate of Origin are subject to its policy.
|
||||
|
||||
There are many ways to contribute to SideStore, so if you aren't a developer, there are still many other ways you can help out:
|
||||
|
||||
- [Writing documentation](https://github.com/SideStore/SideStore-Docs)
|
||||
- [Submitting detailed bug reports and suggesting new features](https://github.com/SideStore/SideStore/issues/new/choose)
|
||||
- Helping out with support
|
||||
- [Discord](https://discord.gg/sidestore-949183273383395328)
|
||||
- [Discord](https://discord.gg/RgpFBX3Q3k)
|
||||
- [GitHub Discussions](https://github.com/SideStore/SideStore/discussions)
|
||||
|
||||
However, this guide will focus on the development side of things. For now, we will only have setup information here, but you can [join our Discord](https://discord.gg/RgpFBX3Q3k) if you need help
|
||||
|
||||
22
Configurations/SideStore.xcconfig
Normal file
22
Configurations/SideStore.xcconfig
Normal file
@@ -0,0 +1,22 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=YES
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon
|
||||
CLANG_ENABLE_MODULES=YES
|
||||
CODE_SIGN_ENTITLEMENTS=Sources/SideStore/Resources/AltStore.entitlements
|
||||
CODE_SIGN_IDENTITY=iPhone Developer
|
||||
CODE_SIGN_STYLE=Automatic
|
||||
DEVELOPMENT_TEAM=$(DEVELOPMENT_TEAM)
|
||||
ENABLE_BITCODE=NO
|
||||
INFOPLIST_FILE=Sources/SideStore/Resources/Info.plist
|
||||
IPHONEOS_DEPLOYMENT_TARGET=14.0
|
||||
PRODUCT_BUNDLE_IDENTIFIER=$(PRODUCT_BUNDLE_IDENTIFIER)
|
||||
PRODUCT_NAME=$(TARGET_NAME)
|
||||
PROVISIONING_PROFILE_SPECIFIER=
|
||||
SUPPORTED_PLATFORMS=iphonesimulator iphoneos
|
||||
SWIFT_VERSION=5.0
|
||||
TARGETED_DEVICE_FAMILY=1,2
|
||||
|
||||
LD_RUNPATH_SEARCH_PATHS[config=Debug]=["$(inherited)", "@executable_path/Frameworks"]
|
||||
LD_RUNPATH_SEARCH_PATHS[config=Release]=["$(inherited)", "@executable_path/Frameworks"]
|
||||
LIBRARY_SEARCH_PATHS[config=Debug]=["$(inherited)", "$(PROJECT_DIR)/Dependencies/fragmentzip", "$(PROJECT_DIR)/Dependencies/libcurl"]
|
||||
LIBRARY_SEARCH_PATHS[config=Release]=["$(inherited)", "$(PROJECT_DIR)/Dependencies/fragmentzip", "$(PROJECT_DIR)/Dependencies/libcurl"]
|
||||
SWIFT_OPTIMIZATION_LEVEL[config=Debug]=-Onone
|
||||
62
Configurations/SideStoreProject.xcconfig
Normal file
62
Configurations/SideStoreProject.xcconfig
Normal file
@@ -0,0 +1,62 @@
|
||||
ALWAYS_SEARCH_USER_PATHS=NO
|
||||
CLANG_ANALYZER_NONNULL=YES
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION=YES_AGGRESSIVE
|
||||
CLANG_CXX_LANGUAGE_STANDARD=gnu++14
|
||||
CLANG_CXX_LIBRARY=libc++
|
||||
CLANG_ENABLE_MODULES=YES
|
||||
CLANG_ENABLE_OBJC_ARC=YES
|
||||
CLANG_ENABLE_OBJC_WEAK=YES
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING=YES
|
||||
CLANG_WARN_BOOL_CONVERSION=YES
|
||||
CLANG_WARN_COMMA=YES
|
||||
CLANG_WARN_CONSTANT_CONVERSION=YES
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS=YES
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE=YES_ERROR
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS=NO
|
||||
CLANG_WARN_EMPTY_BODY=YES
|
||||
CLANG_WARN_ENUM_CONVERSION=YES
|
||||
CLANG_WARN_INFINITE_RECURSION=YES
|
||||
CLANG_WARN_INT_CONVERSION=YES
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION=YES
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF=YES
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION=YES
|
||||
CLANG_WARN_OBJC_ROOT_CLASS=YES_ERROR
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS=YES
|
||||
CLANG_WARN_STRICT_PROTOTYPES=YES
|
||||
CLANG_WARN_SUSPICIOUS_MOVE=YES
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY=YES_AGGRESSIVE
|
||||
CLANG_WARN_UNREACHABLE_CODE=YES
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH=YES
|
||||
CODE_SIGN_IDENTITY=iPhone Developer
|
||||
COPY_PHASE_STRIP=NO
|
||||
DEBUG_INFORMATION_FORMAT=dwarf-with-dsym
|
||||
ENABLE_STRICT_OBJC_MSGSEND=YES
|
||||
GCC_C_LANGUAGE_STANDARD=gnu11
|
||||
GCC_NO_COMMON_BLOCKS=YES
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION=YES
|
||||
GCC_WARN_ABOUT_RETURN_TYPE=YES_ERROR
|
||||
GCC_WARN_UNDECLARED_SELECTOR=YES
|
||||
GCC_WARN_UNINITIALIZED_AUTOS=YES_AGGRESSIVE
|
||||
GCC_WARN_UNUSED_FUNCTION=YES
|
||||
GCC_WARN_UNUSED_VARIABLE=YES
|
||||
IPHONEOS_DEPLOYMENT_TARGET=12.2
|
||||
MTL_FAST_MATH=YES
|
||||
SDKROOT=iphoneos
|
||||
SUPPORTED_PLATFORMS=iphonesimulator iphoneos macosx
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS=DEBUG BETA
|
||||
SYSTEM_HEADER_SEARCH_PATHS="$(SRCROOT)/Dependencies/AltSign/Dependencies"
|
||||
|
||||
ENABLE_NS_ASSERTIONS[config=Release]=NO
|
||||
ENABLE_TESTABILITY[config=Debug]=YES
|
||||
GCC_DYNAMIC_NO_PIC[config=Debug]=NO
|
||||
GCC_OPTIMIZATION_LEVEL[config=Debug]=0
|
||||
GCC_PREPROCESSOR_DEFINITIONS[config=Debug]=["DEBUG=1", "$(inherited)"]
|
||||
MTL_ENABLE_DEBUG_INFO[config=Debug]=INCLUDE_SOURCE
|
||||
MTL_ENABLE_DEBUG_INFO[config=Release]=NO
|
||||
ONLY_ACTIVE_ARCH[config=Debug]=YES
|
||||
OTHER_CPLUSPLUSFLAGS[config=Debug]=["$(OTHER_CFLAGS)", "-Wno-module-import-in-extern-c"]
|
||||
OTHER_CPLUSPLUSFLAGS[config=Release]=["$(OTHER_CFLAGS)", "-Wno-module-import-in-extern-c"]
|
||||
SWIFT_COMPILATION_MODE[config=Release]=wholemodule
|
||||
SWIFT_OPTIMIZATION_LEVEL[config=Debug]=-Onone
|
||||
SWIFT_OPTIMIZATION_LEVEL[config=Release]=-O
|
||||
VALIDATE_PRODUCT[config=Release]=YES
|
||||
15
Dangerfile.swift
Normal file
15
Dangerfile.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import DangerSwiftCoverage // package: https://github.com/f-meloni/danger-swift-coverage.git
|
||||
import DangerSwiftLint // package: https://github.com/ashfurrow/danger-swiftlint.git
|
||||
import DangerXCodeSummary // package: https://github.com/f-meloni/danger-swift-xcodesummary.git
|
||||
|
||||
let danger = Danger()
|
||||
|
||||
// swift run danger-swift [ci, pr] --dangerfile ./Dangerfile.swift
|
||||
// xcodebuild test -scheme DangerSwiftCoverage-Package -derivedDataPath Build/ -enableCodeCoverage YES
|
||||
// (Recommended) Cache the ~/.danger-swift folder
|
||||
|
||||
Coverage.xcodeBuildCoverage(.derivedDataFolder("Build"),
|
||||
minimumCoverage: 50,
|
||||
excludedTargets: ["DangerSwiftCoverageTests.xctest"])
|
||||
|
||||
SwiftLint.lint(directory: "SideStoreApp", configFile: ".swiftlint.yml")
|
||||
1
Dependencies/MarkdownAttributedString
vendored
1
Dependencies/MarkdownAttributedString
vendored
Submodule Dependencies/MarkdownAttributedString deleted from 750e8d5cb4
1
Dependencies/Roxas
vendored
1
Dependencies/Roxas
vendored
Submodule Dependencies/Roxas deleted from 03f6013972
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user