mirror of
https://github.com/SideStore/SideStore.git
synced 2026-03-27 21:05:39 +01:00
Compare commits
7 Commits
feature/Ro
...
feature/Da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d3060df7 | ||
|
|
c59043068e | ||
|
|
65c43d683c | ||
|
|
9e6147c860 | ||
|
|
b3074cadf9 | ||
|
|
9c5c597ce6 | ||
|
|
977a452605 |
21
.codecov.yml
21
.codecov.yml
@@ -1,21 +0,0 @@
|
||||
# https://docs.codecov.io/docs/codecov-yaml
|
||||
|
||||
codecov:
|
||||
require_ci_to_pass: true
|
||||
|
||||
coverage:
|
||||
precision: 2
|
||||
round: down
|
||||
range: "70...100"
|
||||
ignore:
|
||||
- Dependencies
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
if_no_uploads: error
|
||||
changes: true
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
if_no_uploads: error
|
||||
comment: false
|
||||
@@ -1,35 +1,39 @@
|
||||
# http://editorconfig.org
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# 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
|
||||
|
||||
[*.{md,markdown}]
|
||||
trim_trailing_whitespace = false
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,py}]
|
||||
charset = utf-8# 4 space indentation
|
||||
|
||||
[*.{c,h,m,mm}]
|
||||
trim_trailing_whitespace = true
|
||||
# Swift files
|
||||
[*.swift]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8# 4 space indentation
|
||||
|
||||
# 4 space indentation
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Indentation override for all JS under lib directory
|
||||
[lib/**.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
[*.{swift}]
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
[*.{yaml|yml}]
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# .github/workflows/sidestore-project.yml
|
||||
name: SideStore Project
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: tuist/tuist-action@0.13.0
|
||||
with:
|
||||
command: 'build'
|
||||
arguments: ''
|
||||
|
||||
116
.github/workflows/beta.yml
vendored
116
.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,80 +11,74 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
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: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-dSYM
|
||||
path: ./*.dSYM/
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
- 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
|
||||
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: 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!**
|
||||
- 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 }}`
|
||||
## 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 }}`
|
||||
|
||||
13
.github/workflows/danger.yml
vendored
Normal file
13
.github/workflows/danger.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: "Danger Swift"
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Danger JS
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Danger Swift
|
||||
uses: danger/swift@2.0.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
128
.github/workflows/nightly.yml
vendored
128
.github/workflows/nightly.yml
vendored
@@ -2,7 +2,7 @@ name: Nightly SideStore build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -14,87 +14,81 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
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@v3
|
||||
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: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-dSYM
|
||||
path: ./*.dSYM/
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
- 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
|
||||
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: 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 }}).
|
||||
- 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 }}`
|
||||
|
||||
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 }}
|
||||
- name: Reset cache for apps.sidestore.io/nightly
|
||||
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
|
||||
|
||||
8
.github/workflows/pr.yml
vendored
8
.github/workflows/pr.yml
vendored
@@ -39,14 +39,8 @@ jobs:
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
112
.github/workflows/stable.yml
vendored
112
.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,77 +11,71 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
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: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-dSYM
|
||||
path: ./*.dSYM/
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
- 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
|
||||
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: 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_sidestore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
- 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 }}`
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -35,10 +35,10 @@ xcuserdata
|
||||
## AppCode specific
|
||||
.idea/
|
||||
|
||||
.build
|
||||
|
||||
Payload/
|
||||
SideStore.ipa
|
||||
*.dSYM
|
||||
|
||||
Dependencies/.*-prebuilt-fetch-*
|
||||
Dependencies/minimuxer/*
|
||||
Dependencies/em_proxy/*
|
||||
|
||||
27
.gitmodules
vendored
27
.gitmodules
vendored
@@ -1,6 +1,21 @@
|
||||
[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
|
||||
[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/libimobiledevice/libplist.git
|
||||
[submodule "Dependencies/MarkdownAttributedString"]
|
||||
path = Dependencies/MarkdownAttributedString
|
||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||
[submodule "Dependencies/libimobiledevice-glue"]
|
||||
path = Dependencies/libimobiledevice-glue
|
||||
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
||||
[submodule "Dependencies/libfragmentzip"]
|
||||
path = Dependencies/libfragmentzip
|
||||
url = https://github.com/SideStore/libfragmentzip.git
|
||||
|
||||
28
.jazzy.yaml
28
.jazzy.yaml
@@ -1,28 +0,0 @@
|
||||
# ---- About ----
|
||||
module: SideStore
|
||||
module_version: 1.0,0
|
||||
author: SideStore
|
||||
readme: README.md
|
||||
copyright: 'See [license](https://github.com/SideStore/SideStore/blob/develop/LICENSE) for more details.'
|
||||
|
||||
# ---- URLs ----
|
||||
author_url: https://sidestore.io
|
||||
dash_url: https://sidestore.io/docsets/SideStore.xml
|
||||
github_url: https://github.com/SideStore/SideStore/
|
||||
github_file_prefix: https://github.com/SideStore/SideStore/tree/1.0.2/
|
||||
|
||||
# ---- Sources ----
|
||||
source_directory: Sources
|
||||
documentation: .build/x86_64-apple-macosx/debug/SideStore.docc
|
||||
|
||||
# ---- Generation ----
|
||||
clean: true
|
||||
output: docs
|
||||
min_acl: public
|
||||
hide_documentation_coverage: false
|
||||
skip_undocumented: false
|
||||
objc: false
|
||||
swift_version: 5.1.0
|
||||
|
||||
# ---- Formatting ----
|
||||
theme: fullwidth
|
||||
42
.swiftformat
42
.swiftformat
@@ -1,42 +0,0 @@
|
||||
# .swiftformat
|
||||
|
||||
## file options
|
||||
|
||||
--exclude .build,.github,.swiftpm,.vscode,Configurations,Dependencies
|
||||
|
||||
## format options
|
||||
|
||||
--allman false
|
||||
--binarygrouping 4,8
|
||||
--commas always
|
||||
--comments indent
|
||||
--decimalgrouping 3,6
|
||||
--elseposition same-line
|
||||
--empty void
|
||||
--exponentcase lowercase
|
||||
--exponentgrouping disabled
|
||||
--fractiongrouping disabled
|
||||
--header ignore
|
||||
--hexgrouping 4,8
|
||||
--hexliteralcase uppercase
|
||||
--ifdef indent
|
||||
--importgrouping testable-bottom
|
||||
--indent 4
|
||||
--indentcase false
|
||||
--linebreaks lf
|
||||
--maxwidth none
|
||||
--octalgrouping 4,8
|
||||
--operatorfunc spaced
|
||||
--patternlet hoist
|
||||
--ranges spaced
|
||||
--self remove
|
||||
--semicolons inline
|
||||
--stripunusedargs always
|
||||
--swiftversion 5.1
|
||||
--trimwhitespace always
|
||||
--wraparguments preserve
|
||||
--wrapcollections preserve
|
||||
|
||||
## rules
|
||||
|
||||
--enable isEmpty,andOperator,assertionFailures
|
||||
@@ -1,76 +0,0 @@
|
||||
disabled_rules:
|
||||
- block_based_kvo
|
||||
- colon
|
||||
- control_statement
|
||||
- cyclomatic_complexity
|
||||
- discarded_notification_center_observer
|
||||
- file_length
|
||||
- function_parameter_count
|
||||
- generic_type_name
|
||||
- identifier_name
|
||||
- multiple_closures_with_trailing_closure
|
||||
- nesting
|
||||
- switch_case_alignment
|
||||
- todo
|
||||
- type_name
|
||||
- type_body_length
|
||||
- function_body_length
|
||||
- unused_closure_parameter
|
||||
|
||||
# parameterized rules can be customized from this configuration file
|
||||
line_length: 200
|
||||
# parameterized rules are first parameterized as a warning level, then error level.
|
||||
type_body_length:
|
||||
- 300 # warning
|
||||
- 600 # error
|
||||
# parameterized rules are first parameterized as a warning level, then error level.
|
||||
# identifier_name_max_length:
|
||||
# - 40 # warning
|
||||
# - 60 # error
|
||||
# # parameterized rules are first parameterized as a warning level, then error level.
|
||||
# identifier_name_min_length:
|
||||
# - 3 # warning
|
||||
# - 2 # error
|
||||
function_body_length:
|
||||
- 200 # warning
|
||||
- 500 # error
|
||||
large_tuple:
|
||||
- 4 # warning
|
||||
- 6 # error
|
||||
|
||||
opt_in_rules:
|
||||
- empty_count
|
||||
- force_unwrapping
|
||||
|
||||
excluded: # paths to ignore during linting. overridden byincluded.
|
||||
- .build
|
||||
- .github
|
||||
- .swiftpm
|
||||
- .vscode
|
||||
- Dependencies
|
||||
|
||||
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
|
||||
- explicit_self
|
||||
|
||||
# Override these rules to be warnings for now
|
||||
force_cast: warning
|
||||
force_try: warning
|
||||
empty_count: warning
|
||||
|
||||
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit)
|
||||
|
||||
custom_rules:
|
||||
placeholders_in_comments:
|
||||
included: ".*\\.swift"
|
||||
name: "No Placeholders in Comments"
|
||||
regex: "<#([^#]+)#>"
|
||||
match_kinds:
|
||||
- comment
|
||||
- doccomment
|
||||
message: "Placeholder left in comment."
|
||||
tiles_deprecated:
|
||||
included: ".*\\.swift"
|
||||
name: "Tiles are deprecated in favor of Frame"
|
||||
regex: "([T,t]ile$|^[T,t]il[e,es])"
|
||||
message: "Tiles are deprecated in favor of Frame"
|
||||
severity: warning
|
||||
3
AltBackup.xcconfig
Normal file
3
AltBackup.xcconfig
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup
|
||||
@@ -7,109 +7,115 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
|
||||
extension AppDelegate {
|
||||
extension AppDelegate
|
||||
{
|
||||
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
|
||||
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
|
||||
|
||||
|
||||
static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
|
||||
|
||||
|
||||
static let operationResultKey = "result"
|
||||
}
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
private var currentBackupReturnURL: URL?
|
||||
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
// Override point for customization after application launch.
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
|
||||
|
||||
|
||||
let viewController = ViewController()
|
||||
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window?.rootViewController = viewController
|
||||
window?.makeKeyAndVisible()
|
||||
|
||||
|
||||
self.window = UIWindow(frame: UIScreen.main.bounds)
|
||||
self.window?.rootViewController = viewController
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_: UIApplication) {
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_: UIApplication) {
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_: UIApplication) {
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
||||
open(url)
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||
{
|
||||
return self.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate {
|
||||
func open(_ url: URL) -> Bool {
|
||||
private extension AppDelegate
|
||||
{
|
||||
func open(_ url: URL) -> Bool
|
||||
{
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||
guard let command = components.host?.lowercased() else { return false }
|
||||
|
||||
switch command {
|
||||
|
||||
switch command
|
||||
{
|
||||
case "backup":
|
||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||
currentBackupReturnURL = returnURL
|
||||
self.currentBackupReturnURL = returnURL
|
||||
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
case "restore":
|
||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||
currentBackupReturnURL = returnURL
|
||||
self.currentBackupReturnURL = returnURL
|
||||
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func operationDidFinish(_ notification: Notification) {
|
||||
|
||||
@objc func operationDidFinish(_ notification: Notification)
|
||||
{
|
||||
defer { self.currentBackupReturnURL = nil }
|
||||
|
||||
|
||||
guard
|
||||
let returnURL = currentBackupReturnURL,
|
||||
let returnURL = self.currentBackupReturnURL,
|
||||
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
||||
else { return }
|
||||
|
||||
|
||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
|
||||
|
||||
switch result {
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
components.path = "/success"
|
||||
|
||||
case let .failure(error as NSError):
|
||||
|
||||
case .failure(let error as NSError):
|
||||
components.path = "/failure"
|
||||
components.queryItems = ["errorDomain": error.domain,
|
||||
"errorCode": String(error.code),
|
||||
"errorDescription": error.localizedDescription].map { URLQueryItem(name: $0, value: $1) }
|
||||
}
|
||||
|
||||
|
||||
guard let responseURL = components.url else { return }
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(responseURL, options: [:]) { success in
|
||||
os_log("Sent response to app with success: %@", type: .info , success)
|
||||
UIApplication.shared.open(responseURL, options: [:]) { (success) in
|
||||
print("Sent response to app with success:", success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,15 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
|
||||
import AltSign
|
||||
import Roxas
|
||||
import protocol SideStoreCore.ALTLocalizedError
|
||||
|
||||
extension ErrorUserInfoKey {
|
||||
extension ErrorUserInfoKey
|
||||
{
|
||||
static let sourceFile: String = "alt_sourceFile"
|
||||
static let sourceFileLine: String = "alt_sourceFileLine"
|
||||
}
|
||||
|
||||
extension Error {
|
||||
extension Error
|
||||
{
|
||||
var sourceDescription: String? {
|
||||
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
|
||||
return nil
|
||||
@@ -30,225 +24,267 @@ extension Error {
|
||||
}
|
||||
}
|
||||
|
||||
struct BackupError: ALTLocalizedError {
|
||||
enum Code {
|
||||
struct BackupError: ALTLocalizedError
|
||||
{
|
||||
enum Code
|
||||
{
|
||||
case invalidBundleID
|
||||
case appGroupNotFound(String?)
|
||||
case randomError // Used for debugging.
|
||||
}
|
||||
|
||||
|
||||
let code: Code
|
||||
|
||||
|
||||
let sourceFile: String
|
||||
let sourceFileLine: Int
|
||||
|
||||
|
||||
var failure: String?
|
||||
|
||||
var failureReason: String? {
|
||||
switch code {
|
||||
switch self.code
|
||||
{
|
||||
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
|
||||
case let .appGroupNotFound(appGroup):
|
||||
if let appGroup = appGroup {
|
||||
case .appGroupNotFound(let appGroup):
|
||||
if let appGroup = appGroup
|
||||
{
|
||||
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
|
||||
}
|
||||
case .randomError: return NSLocalizedString("A random error occured.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var errorUserInfo: [String: Any] {
|
||||
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: errorDescription,
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
NSLocalizedFailureErrorKey: failure,
|
||||
ErrorUserInfoKey.sourceFile: sourceFile,
|
||||
ErrorUserInfoKey.sourceFileLine: sourceFileLine]
|
||||
|
||||
var errorUserInfo: [String : Any] {
|
||||
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription,
|
||||
NSLocalizedFailureReasonErrorKey: self.failureReason,
|
||||
NSLocalizedFailureErrorKey: self.failure,
|
||||
ErrorUserInfoKey.sourceFile: self.sourceFile,
|
||||
ErrorUserInfoKey.sourceFileLine: self.sourceFileLine]
|
||||
return userInfo.compactMapValues { $0 }
|
||||
}
|
||||
|
||||
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line) {
|
||||
|
||||
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
|
||||
{
|
||||
self.code = code
|
||||
failure = description
|
||||
sourceFile = file
|
||||
sourceFileLine = line
|
||||
self.failure = description
|
||||
self.sourceFile = file
|
||||
self.sourceFileLine = line
|
||||
}
|
||||
}
|
||||
|
||||
class BackupController: NSObject {
|
||||
class BackupController: NSObject
|
||||
{
|
||||
private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
override init() {
|
||||
operationQueue.name = "AltBackup-BackupQueue"
|
||||
|
||||
override init()
|
||||
{
|
||||
self.operationQueue.name = "AltBackup-BackupQueue"
|
||||
}
|
||||
|
||||
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
do {
|
||||
|
||||
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
|
||||
}
|
||||
|
||||
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) }
|
||||
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
|
||||
|
||||
// Use temporary directory to prevent messing up successful backup with incomplete one.
|
||||
let temporaryAppBackupDirectory = backupsDirectory.appendingPathComponent("Temp", isDirectory: true).appendingPathComponent(UUID().uuidString)
|
||||
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
||||
|
||||
|
||||
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
|
||||
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
|
||||
fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: operationQueue) { error in
|
||||
do {
|
||||
if let error = error {
|
||||
self.fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: self.operationQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
do
|
||||
{
|
||||
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
|
||||
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path) {
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: backupDocumentsDirectory)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: documentsDirectory.path) {
|
||||
|
||||
if FileManager.default.fileExists(atPath: documentsDirectory.path)
|
||||
{
|
||||
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
|
||||
}
|
||||
|
||||
|
||||
print("Copied Documents directory from \(documentsDirectory) to \(backupDocumentsDirectory)")
|
||||
|
||||
|
||||
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
||||
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path) {
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: backupLibraryDirectory)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: libraryDirectory.path) {
|
||||
|
||||
if FileManager.default.fileExists(atPath: libraryDirectory.path)
|
||||
{
|
||||
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
|
||||
}
|
||||
|
||||
|
||||
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
|
||||
}
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup {
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
|
||||
{
|
||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
|
||||
}
|
||||
|
||||
|
||||
let backupAppGroupURL = temporaryAppBackupDirectory.appendingPathComponent(appGroup)
|
||||
|
||||
|
||||
// There are several system hidden files that we don't have permission to read, so we just skip all hidden files in app group directories.
|
||||
try self.copyDirectoryContents(at: appGroupURL, to: backupAppGroupURL, options: [.skipsHiddenFiles])
|
||||
}
|
||||
|
||||
|
||||
// Replace previous backup with new backup.
|
||||
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
|
||||
|
||||
os_log("Replaced previous backup with new backup: %@", type: .info, temporaryAppBackupDirectory.absoluteString)
|
||||
|
||||
|
||||
print("Replaced previous backup with new backup:", temporaryAppBackupDirectory)
|
||||
|
||||
completionHandler(.success(()))
|
||||
} catch {
|
||||
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) } catch { os_log("Failed to remove temporary directory. %@", type: .error , error.localizedDescription) }
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) }
|
||||
catch { print("Failed to remove temporary directory.", error) }
|
||||
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
do {
|
||||
|
||||
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
|
||||
}
|
||||
|
||||
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to access backup.", comment: "")) }
|
||||
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
||||
|
||||
|
||||
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
|
||||
fileCoordinator.coordinate(with: [readingIntent], queue: operationQueue) { error in
|
||||
do {
|
||||
if let error = error {
|
||||
self.fileCoordinator.coordinate(with: [readingIntent], queue: self.operationQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
|
||||
let mainGroupBackupDirectory = appBackupDirectory.appendingPathComponent("App")
|
||||
|
||||
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
||||
|
||||
|
||||
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
||||
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
||||
|
||||
|
||||
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
|
||||
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup {
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
|
||||
{
|
||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
|
||||
}
|
||||
|
||||
|
||||
let backupAppGroupURL = appBackupDirectory.appendingPathComponent(appGroup)
|
||||
try self.copyDirectoryContents(at: backupAppGroupURL, to: appGroupURL)
|
||||
}
|
||||
|
||||
|
||||
completionHandler(.success(()))
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension BackupController {
|
||||
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws {
|
||||
private extension BackupController
|
||||
{
|
||||
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws
|
||||
{
|
||||
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
|
||||
|
||||
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path) {
|
||||
|
||||
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path)
|
||||
{
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options) {
|
||||
|
||||
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options)
|
||||
{
|
||||
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
|
||||
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path)
|
||||
{
|
||||
do {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
} catch CocoaError.fileWriteNoPermission where isDirectory {
|
||||
try copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
|
||||
}
|
||||
catch CocoaError.fileWriteNoPermission where isDirectory {
|
||||
try self.copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
|
||||
continue
|
||||
} catch {
|
||||
}
|
||||
catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||
print("Copied item from \(fileURL) to \(destinationURL)")
|
||||
} catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
|
||||
}
|
||||
catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
|
||||
// Ignore errors for /Documents/Inbox
|
||||
os_log("Failed to copy Inbox directory: %@", type: .error , error.localizedDescription)
|
||||
} catch {
|
||||
print("Failed to copy Inbox directory:", error)
|
||||
}
|
||||
catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
@@ -35,16 +35,6 @@
|
||||
<string>altbackup</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>SideBackup General</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>sidebackup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
extension UIColor
|
||||
{
|
||||
static let altstoreBackground = UIColor(named: "Background")!
|
||||
static let altstoreText = UIColor(named: "Text")!
|
||||
}
|
||||
206
AltBackup/ViewController.swift
Normal file
206
AltBackup/ViewController.swift
Normal file
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension Bundle
|
||||
{
|
||||
var appName: String? {
|
||||
let appName =
|
||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ??
|
||||
Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
|
||||
return appName
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewController
|
||||
{
|
||||
enum BackupOperation
|
||||
{
|
||||
case backup
|
||||
case restore
|
||||
}
|
||||
}
|
||||
|
||||
class ViewController: UIViewController
|
||||
{
|
||||
private let backupController = BackupController()
|
||||
|
||||
private var currentOperation: BackupOperation? {
|
||||
didSet {
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var textLabel: UILabel!
|
||||
private var detailTextLabel: UILabel!
|
||||
private var activityIndicatorView: UIActivityIndicatorView!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
||||
{
|
||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.backgroundColor = .altstoreBackground
|
||||
|
||||
self.textLabel = UILabel(frame: .zero)
|
||||
self.textLabel.font = UIFont.preferredFont(forTextStyle: .title2)
|
||||
self.textLabel.textColor = .altstoreText
|
||||
self.textLabel.textAlignment = .center
|
||||
self.textLabel.numberOfLines = 0
|
||||
|
||||
self.detailTextLabel = UILabel(frame: .zero)
|
||||
self.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
self.detailTextLabel.textColor = .altstoreText
|
||||
self.detailTextLabel.textAlignment = .center
|
||||
self.detailTextLabel.numberOfLines = 0
|
||||
|
||||
self.activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
|
||||
self.activityIndicatorView.color = .altstoreText
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
#if DEBUG
|
||||
let button1 = UIButton(type: .system)
|
||||
button1.setTitle("Backup", for: .normal)
|
||||
button1.setTitleColor(.white, for: .normal)
|
||||
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||
|
||||
let button2 = UIButton(type: .system)
|
||||
button2.setTitle("Restore", for: .normal)
|
||||
button2.setTitleColor(.white, for: .normal)
|
||||
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
||||
#else
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
|
||||
#endif
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.spacing = 22
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
self.view.addSubview(stackView)
|
||||
|
||||
NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
|
||||
stackView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0),
|
||||
self.view.safeAreaLayoutGuide.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)])
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private extension ViewController
|
||||
{
|
||||
@objc func backup()
|
||||
{
|
||||
self.currentOperation = .backup
|
||||
|
||||
self.backupController.performBackup { (result) in
|
||||
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||
|
||||
let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName)
|
||||
self.process(result, errorTitle: title)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restore()
|
||||
{
|
||||
self.currentOperation = .restore
|
||||
|
||||
self.backupController.restoreBackup { (result) in
|
||||
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||
|
||||
let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName)
|
||||
self.process(result, errorTitle: title)
|
||||
}
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
switch self.currentOperation
|
||||
{
|
||||
case .backup:
|
||||
self.textLabel.text = NSLocalizedString("Backing up app data…", comment: "")
|
||||
self.detailTextLabel.isHidden = true
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
case .restore:
|
||||
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
|
||||
self.detailTextLabel.isHidden = true
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
case .none:
|
||||
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
|
||||
|
||||
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in SideStore to continue using it.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
|
||||
|
||||
self.detailTextLabel.isHidden = false
|
||||
self.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ViewController
|
||||
{
|
||||
func process(_ result: Result<Void, Error>, errorTitle: String)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error as NSError):
|
||||
let message: String
|
||||
|
||||
if let sourceDescription = error.sourceDescription
|
||||
{
|
||||
message = error.localizedDescription + "\n\n" + sourceDescription
|
||||
}
|
||||
else
|
||||
{
|
||||
message = error.localizedDescription
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func didEnterBackground(_ notification: Notification)
|
||||
{
|
||||
// Reset UI once we've left app (but not before).
|
||||
self.currentOperation = nil
|
||||
}
|
||||
}
|
||||
@@ -10,41 +10,46 @@ import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension UserDefaults {
|
||||
private extension UserDefaults
|
||||
{
|
||||
@objc var localUserID: String? {
|
||||
get { string(forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
set { set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
}
|
||||
}
|
||||
|
||||
struct AnisetteDataManager {
|
||||
struct AnisetteDataManager
|
||||
{
|
||||
static let shared = AnisetteDataManager()
|
||||
|
||||
|
||||
private let dateFormatter = ISO8601DateFormatter()
|
||||
|
||||
private init() {
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW)
|
||||
|
||||
private init()
|
||||
{
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
||||
}
|
||||
|
||||
func requestAnisetteData() throws -> ALTAnisetteData {
|
||||
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
||||
|
||||
func requestAnisetteData() throws -> ALTAnisetteData
|
||||
{
|
||||
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
|
||||
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
|
||||
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
|
||||
|
||||
|
||||
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
|
||||
let headers = session.appleIDHeaders(for: request)
|
||||
|
||||
|
||||
let device = akDevice.current
|
||||
let date = dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
||||
|
||||
let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
||||
|
||||
var localUserID = UserDefaults.standard.localUserID
|
||||
if localUserID == nil {
|
||||
if localUserID == nil
|
||||
{
|
||||
localUserID = UUID().uuidString
|
||||
UserDefaults.standard.localUserID = localUserID
|
||||
}
|
||||
|
||||
|
||||
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
|
||||
oneTimePassword: headers["X-Apple-I-MD"] ?? "",
|
||||
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",
|
||||
@@ -10,105 +10,127 @@ import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension URL {
|
||||
private extension URL
|
||||
{
|
||||
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
|
||||
}
|
||||
|
||||
private extension CFNotificationName {
|
||||
private extension CFNotificationName
|
||||
{
|
||||
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
|
||||
}
|
||||
|
||||
struct AppManager {
|
||||
struct AppManager
|
||||
{
|
||||
static let shared = AppManager()
|
||||
|
||||
|
||||
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
|
||||
private let profilesQueue = OperationQueue()
|
||||
|
||||
|
||||
private let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
private init() {
|
||||
profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
||||
profilesQueue.qualityOfService = .userInitiated
|
||||
|
||||
private init()
|
||||
{
|
||||
self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
||||
self.profilesQueue.qualityOfService = .userInitiated
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles _: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
appQueue.async {
|
||||
|
||||
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
|
||||
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String: Any]
|
||||
|
||||
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
|
||||
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
|
||||
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
||||
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
appQueue.async {
|
||||
|
||||
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
|
||||
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in
|
||||
do {
|
||||
if let error = error {
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
|
||||
let installingBundleIDs = Set(profiles.map(\.bundleIdentifier))
|
||||
|
||||
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
|
||||
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
|
||||
for fileURL in profileURLs {
|
||||
for fileURL in profileURLs
|
||||
{
|
||||
// Use memory mapping to reduce peak memory usage and stay within limit.
|
||||
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
|
||||
|
||||
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile) {
|
||||
|
||||
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
} else {
|
||||
os_log("Ignoring: %@ %@", type: .info , profile.bundleIdentifier, profile.uuid)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
for profile in profiles {
|
||||
|
||||
for profile in profiles
|
||||
{
|
||||
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
|
||||
try profile.data.write(to: destinationURL, options: .atomic)
|
||||
}
|
||||
|
||||
|
||||
completionHandler(.success(()))
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in
|
||||
do {
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||
do
|
||||
{
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
for fileURL in profileURLs {
|
||||
|
||||
for fileURL in profileURLs
|
||||
{
|
||||
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
||||
|
||||
if bundleIdentifiers.contains(profile.bundleIdentifier) {
|
||||
|
||||
if bundleIdentifiers.contains(profile.bundleIdentifier)
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
completionHandler(.success(()))
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
@@ -8,103 +8,113 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import SideKit
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
|
||||
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
|
||||
|
||||
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
|
||||
connectionHandlers: [XPCConnectionHandler()])
|
||||
|
||||
extension DaemonConnectionManager {
|
||||
extension DaemonConnectionManager
|
||||
{
|
||||
static var shared: ConnectionManager {
|
||||
connectionManager
|
||||
return connectionManager
|
||||
}
|
||||
}
|
||||
|
||||
struct DaemonRequestHandler: RequestHandler {
|
||||
func handleAnisetteDataRequest(_: AnisetteDataRequest, for _: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void) {
|
||||
do {
|
||||
struct DaemonRequestHandler: RequestHandler
|
||||
{
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
|
||||
|
||||
|
||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||
completionHandler(.success(response))
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void) {
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
||||
{
|
||||
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
|
||||
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
connection.receiveRequest { result in
|
||||
os_log("Received begin installation request with result: %@", type: .info , String(describing: result))
|
||||
|
||||
do {
|
||||
guard case let .beginInstallation(request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
||||
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
do
|
||||
{
|
||||
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
||||
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
|
||||
|
||||
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { result in
|
||||
|
||||
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
|
||||
let result = result.map { InstallationProgressResponse(progress: 1.0) }
|
||||
os_log("Installed app with result: %@", type: .info, String(describing: result))
|
||||
|
||||
print("Installed app with result:", result)
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for _: Connection,
|
||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void) {
|
||||
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
os_log("Failed to install profiles %@ : %@", type: .error , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n"), error.localizedDescription)
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
|
||||
case .success:
|
||||
os_log("Installed profiles: %@", type: .info , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n"))
|
||||
|
||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||
|
||||
let response = InstallProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for _: Connection,
|
||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void) {
|
||||
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
os_log("Failed to remove profiles %@ : %@", type: .error, request.bundleIdentifiers, error.localizedDescription)
|
||||
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
|
||||
case .success:
|
||||
os_log("Removed profiles: %@", type: .info , request.bundleIdentifiers)
|
||||
|
||||
print("Removed profiles:", request.bundleIdentifiers)
|
||||
|
||||
let response = RemoveProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for _: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void) {
|
||||
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
os_log("Failed to remove app %@ : %@", type: .error , request.bundleIdentifier, error.localizedDescription)
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
|
||||
case .success:
|
||||
os_log("Removed app: %@", type: .info , request.bundleIdentifier)
|
||||
|
||||
print("Removed app:", request.bundleIdentifier)
|
||||
|
||||
let response = RemoveAppResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
@@ -8,78 +8,86 @@
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
import SideStoreCore
|
||||
|
||||
class XPCConnectionHandler: NSObject, ConnectionHandler {
|
||||
class XPCConnectionHandler: NSObject, ConnectionHandler
|
||||
{
|
||||
var connectionHandler: ((Connection) -> Void)?
|
||||
var disconnectionHandler: ((Connection) -> Void)?
|
||||
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
|
||||
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
|
||||
|
||||
deinit {
|
||||
|
||||
deinit
|
||||
{
|
||||
self.stopListening()
|
||||
}
|
||||
|
||||
func startListening() {
|
||||
for listener in listeners {
|
||||
|
||||
func startListening()
|
||||
{
|
||||
for listener in self.listeners
|
||||
{
|
||||
listener.delegate = self
|
||||
listener.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func stopListening() {
|
||||
listeners.forEach { $0.suspend() }
|
||||
|
||||
func stopListening()
|
||||
{
|
||||
self.listeners.forEach { $0.suspend() }
|
||||
}
|
||||
}
|
||||
|
||||
private extension XPCConnectionHandler {
|
||||
func disconnect(_ connection: Connection) {
|
||||
private extension XPCConnectionHandler
|
||||
{
|
||||
func disconnect(_ connection: Connection)
|
||||
{
|
||||
connection.disconnect()
|
||||
|
||||
disconnectionHandler?(connection)
|
||||
|
||||
self.disconnectionHandler?(connection)
|
||||
}
|
||||
}
|
||||
|
||||
extension XPCConnectionHandler: NSXPCListenerDelegate {
|
||||
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
||||
extension XPCConnectionHandler: NSXPCListenerDelegate
|
||||
{
|
||||
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
|
||||
{
|
||||
let maximumPathLength = 4 * UInt32(MAXPATHLEN)
|
||||
|
||||
|
||||
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
|
||||
defer { pathBuffer.deallocate() }
|
||||
|
||||
|
||||
proc_pidpath(newConnection.processIdentifier, pathBuffer, maximumPathLength)
|
||||
|
||||
|
||||
let path = String(cString: pathBuffer)
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
|
||||
|
||||
var code: UnsafeMutableRawPointer?
|
||||
defer { code.map { Unmanaged<AnyObject>.fromOpaque($0).release() } }
|
||||
|
||||
|
||||
var status = SecStaticCodeCreateWithPath(fileURL as CFURL, 0, &code)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
|
||||
var signingInfo: CFDictionary?
|
||||
defer { signingInfo.map { Unmanaged<AnyObject>.passUnretained($0).release() } }
|
||||
|
||||
|
||||
status = SecCodeCopySigningInformation(code, kSecCSInternalInformation | kSecCSSigningInformation, &signingInfo)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
|
||||
// Only accept connections from AltStore.
|
||||
guard
|
||||
let codeSigningInfo = signingInfo as? [String: Any],
|
||||
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
||||
bundleIdentifier.contains(Bundle.Info.appbundleIdentifier)
|
||||
else { return false }
|
||||
|
||||
|
||||
let connection = XPCConnection(newConnection)
|
||||
newConnection.invalidationHandler = { [weak self, weak connection] in
|
||||
guard let self = self, let connection = connection else { return }
|
||||
self.disconnect(connection)
|
||||
}
|
||||
|
||||
connectionHandler?(connection)
|
||||
|
||||
self.connectionHandler?(connection)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
3
AltStore.xcconfig
Normal file
3
AltStore.xcconfig
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -63,15 +63,6 @@
|
||||
"version" : "1.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "roxas",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/JoeMatt/Roxas.git",
|
||||
"state" : {
|
||||
"revision" : "17338c09ec0ffeea4c68135f17c1f801a3d6d10d",
|
||||
"version" : "1.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "semanticversion",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -81,15 +72,6 @@
|
||||
"version" : "0.3.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sidekit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SideStore/SideKit.git",
|
||||
"state" : {
|
||||
"revision" : "7ea34a09b52c104077dea8e0b90f8dc55d43b36b",
|
||||
"version" : "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -31,7 +31,7 @@
|
||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||
BuildableName = "libAltKit.a"
|
||||
BlueprintName = "AltKit"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
@@ -43,9 +43,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "SideDaemon"
|
||||
BlueprintName = "SideDaemon"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -72,9 +72,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "SideDaemon"
|
||||
BlueprintName = "SideDaemon"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
@@ -95,9 +95,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "SideDaemon"
|
||||
BlueprintName = "SideDaemon"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
@@ -17,7 +17,7 @@
|
||||
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
||||
BuildableName = "AltPlugin.mailbundle"
|
||||
BlueprintName = "AltPlugin"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -53,7 +53,7 @@
|
||||
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
||||
BuildableName = "AltPlugin.mailbundle"
|
||||
BlueprintName = "AltPlugin"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
@@ -17,7 +17,7 @@
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -27,17 +27,19 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -56,9 +58,11 @@
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -73,7 +77,7 @@
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
@@ -17,7 +17,7 @@
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -47,7 +47,7 @@
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
@@ -70,7 +70,7 @@
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
@@ -17,7 +17,7 @@
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -47,7 +47,7 @@
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
@@ -70,7 +70,7 @@
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
@@ -17,7 +17,7 @@
|
||||
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||
BuildableName = "AltXPC.xpc"
|
||||
BlueprintName = "AltXPC"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -47,7 +47,7 @@
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
@@ -63,7 +63,7 @@
|
||||
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||
BuildableName = "AltXPC.xpc"
|
||||
BlueprintName = "AltXPC"
|
||||
ReferencedContainer = "container:SideStore.xcodeproj">
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
8
AltStore/AltStore-Bridging-Header.h
Normal file
8
AltStore/AltStore-Bridging-Header.h
Normal file
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "NSAttributedString+Markdown.h"
|
||||
#import "ALTAppPatcher.h"
|
||||
|
||||
#include "fragmentzip.h"
|
||||
@@ -4,8 +4,6 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.siri</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import SideStoreCore
|
||||
import AltStoreCore
|
||||
|
||||
import AppCenter
|
||||
import AppCenterAnalytics
|
||||
@@ -16,8 +16,10 @@ import AppCenterCrashes
|
||||
|
||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||
|
||||
public extension AnalyticsManager {
|
||||
enum EventProperty: String {
|
||||
extension AnalyticsManager
|
||||
{
|
||||
enum EventProperty: String
|
||||
{
|
||||
case name
|
||||
case bundleIdentifier
|
||||
case developerName
|
||||
@@ -27,28 +29,31 @@ public extension AnalyticsManager {
|
||||
case sourceIdentifier
|
||||
case sourceURL
|
||||
}
|
||||
|
||||
enum Event {
|
||||
|
||||
enum Event
|
||||
{
|
||||
case installedApp(InstalledApp)
|
||||
case updatedApp(InstalledApp)
|
||||
case refreshedApp(InstalledApp)
|
||||
|
||||
public var name: String {
|
||||
switch self {
|
||||
|
||||
var name: String {
|
||||
switch self
|
||||
{
|
||||
case .installedApp: return "installed_app"
|
||||
case .updatedApp: return "updated_app"
|
||||
case .refreshedApp: return "refreshed_app"
|
||||
}
|
||||
}
|
||||
|
||||
public var properties: [EventProperty: String] {
|
||||
|
||||
var properties: [EventProperty: String] {
|
||||
let properties: [EventProperty: String?]
|
||||
|
||||
switch self {
|
||||
case let .installedApp(app), let .updatedApp(app), let .refreshedApp(app):
|
||||
|
||||
switch self
|
||||
{
|
||||
case .installedApp(let app), .updatedApp(let app), .refreshedApp(let app):
|
||||
let appBundleURL = InstalledApp.fileURL(for: app)
|
||||
let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
|
||||
|
||||
|
||||
properties = [
|
||||
.name: app.name,
|
||||
.bundleIdentifier: app.bundleIdentifier,
|
||||
@@ -60,31 +65,37 @@ public extension AnalyticsManager {
|
||||
.sourceURL: app.storeApp?.source?.sourceURL.absoluteString
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
return properties.compactMapValues { $0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class AnalyticsManager {
|
||||
public static let shared = AnalyticsManager()
|
||||
|
||||
private init() {}
|
||||
final class AnalyticsManager
|
||||
{
|
||||
static let shared = AnalyticsManager()
|
||||
|
||||
private init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public extension AnalyticsManager {
|
||||
func start() {
|
||||
extension AnalyticsManager
|
||||
{
|
||||
func start()
|
||||
{
|
||||
AppCenter.start(withAppSecret: appCenterAppSecret, services: [
|
||||
Analytics.self,
|
||||
Crashes.self
|
||||
])
|
||||
}
|
||||
|
||||
func trackEvent(_ event: Event) {
|
||||
let properties = event.properties.reduce(into: [:]) { properties, item in
|
||||
|
||||
func trackEvent(_ event: Event)
|
||||
{
|
||||
let properties = event.properties.reduce(into: [:]) { (properties, item) in
|
||||
properties[item.key.rawValue] = item.value
|
||||
}
|
||||
|
||||
|
||||
Analytics.trackEvent(event.name, withProperties: properties)
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,15 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import SideStoreCore
|
||||
import RoxasUIKit
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
extension AppContentViewController {
|
||||
private enum Row: Int, CaseIterable {
|
||||
extension AppContentViewController
|
||||
{
|
||||
private enum Row: Int, CaseIterable
|
||||
{
|
||||
case subtitle
|
||||
case screenshots
|
||||
case description
|
||||
@@ -27,169 +25,186 @@ extension AppContentViewController {
|
||||
}
|
||||
}
|
||||
|
||||
final class AppContentViewController: UITableViewController {
|
||||
final class AppContentViewController: UITableViewController
|
||||
{
|
||||
var app: StoreApp!
|
||||
|
||||
|
||||
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
||||
private lazy var permissionsDataSource = self.makePermissionsDataSource()
|
||||
|
||||
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
|
||||
private lazy var byteCountFormatter: ByteCountFormatter = {
|
||||
let formatter = ByteCountFormatter()
|
||||
return formatter
|
||||
}()
|
||||
|
||||
|
||||
@IBOutlet private var subtitleLabel: UILabel!
|
||||
@IBOutlet private var descriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
@IBOutlet private var versionDateLabel: UILabel!
|
||||
@IBOutlet private var sizeLabel: UILabel!
|
||||
|
||||
|
||||
@IBOutlet private var screenshotsCollectionView: UICollectionView!
|
||||
@IBOutlet private var permissionsCollectionView: UICollectionView!
|
||||
|
||||
var preferredScreenshotSize: CGSize? {
|
||||
|
||||
var preferredScreenshotSize: CGSize? {
|
||||
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
|
||||
|
||||
let aspectRatio: CGFloat = 16.0 / 9.0 // Hardcoded for now.
|
||||
|
||||
|
||||
let width = self.screenshotsCollectionView.bounds.width - (layout.minimumInteritemSpacing * 2)
|
||||
|
||||
|
||||
let itemWidth = width / 1.5
|
||||
let itemHeight = itemWidth * aspectRatio
|
||||
|
||||
|
||||
return CGSize(width: itemWidth, height: itemHeight)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.contentInset.bottom = 20
|
||||
|
||||
screenshotsCollectionView.dataSource = screenshotsDataSource
|
||||
screenshotsCollectionView.prefetchDataSource = screenshotsDataSource
|
||||
|
||||
permissionsCollectionView.dataSource = permissionsDataSource
|
||||
|
||||
subtitleLabel.text = app.subtitle
|
||||
descriptionTextView.text = app.localizedDescription
|
||||
|
||||
if let version = app.latestVersion {
|
||||
versionDescriptionTextView.text = version.localizedDescription
|
||||
versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
||||
versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: dateFormatter)
|
||||
sizeLabel.text = byteCountFormatter.string(fromByteCount: version.size)
|
||||
} else {
|
||||
versionDescriptionTextView.text = nil
|
||||
versionLabel.text = nil
|
||||
versionDateLabel.text = nil
|
||||
sizeLabel.text = byteCountFormatter.string(fromByteCount: 0)
|
||||
|
||||
self.tableView.contentInset.bottom = 20
|
||||
|
||||
self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
|
||||
self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
|
||||
|
||||
self.permissionsCollectionView.dataSource = self.permissionsDataSource
|
||||
|
||||
self.subtitleLabel.text = self.app.subtitle
|
||||
self.descriptionTextView.text = self.app.localizedDescription
|
||||
|
||||
if let version = self.app.latestVersion
|
||||
{
|
||||
self.versionDescriptionTextView.text = version.localizedDescription
|
||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
||||
}
|
||||
|
||||
descriptionTextView.maximumNumberOfLines = 5
|
||||
descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
|
||||
versionDescriptionTextView.maximumNumberOfLines = 3
|
||||
versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
else
|
||||
{
|
||||
self.versionDescriptionTextView.text = nil
|
||||
self.versionLabel.text = nil
|
||||
self.versionDateLabel.text = nil
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
|
||||
}
|
||||
|
||||
self.descriptionTextView.maximumNumberOfLines = 5
|
||||
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
|
||||
self.versionDescriptionTextView.maximumNumberOfLines = 3
|
||||
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
guard var size = preferredScreenshotSize else { return }
|
||||
size.height = min(size.height, screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
|
||||
|
||||
let layout = screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
|
||||
guard var size = self.preferredScreenshotSize else { return }
|
||||
size.height = min(size.height, self.screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
|
||||
|
||||
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
layout.itemSize = size
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
{
|
||||
guard segue.identifier == "showPermission" else { return }
|
||||
|
||||
guard let cell = sender as? UICollectionViewCell, let indexPath = permissionsCollectionView.indexPath(for: cell) else { return }
|
||||
|
||||
let permission = permissionsDataSource.item(at: indexPath)
|
||||
|
||||
let maximumWidth = view.bounds.width - 20
|
||||
|
||||
|
||||
guard let cell = sender as? UICollectionViewCell, let indexPath = self.permissionsCollectionView.indexPath(for: cell) else { return }
|
||||
|
||||
let permission = self.permissionsDataSource.item(at: indexPath)
|
||||
|
||||
let maximumWidth = self.view.bounds.width - 20
|
||||
|
||||
let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController
|
||||
permissionPopoverViewController.permission = permission
|
||||
permissionPopoverViewController.view.widthAnchor.constraint(lessThanOrEqualToConstant: maximumWidth).isActive = true
|
||||
|
||||
|
||||
let size = permissionPopoverViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
permissionPopoverViewController.preferredContentSize = size
|
||||
|
||||
|
||||
permissionPopoverViewController.popoverPresentationController?.delegate = self
|
||||
permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame
|
||||
permissionPopoverViewController.popoverPresentationController?.sourceView = permissionsCollectionView
|
||||
permissionPopoverViewController.popoverPresentationController?.sourceView = self.permissionsCollectionView
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppContentViewController {
|
||||
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> {
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: app.screenshotURLs as [NSURL])
|
||||
dataSource.cellConfigurationHandler = { cell, _, _ in
|
||||
private extension AppContentViewController
|
||||
{
|
||||
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
|
||||
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.image = nil
|
||||
cell.imageView.isIndicatingActivity = true
|
||||
}
|
||||
dataSource.prefetchHandler = { imageURL, _, completionHandler in
|
||||
RSTAsyncBlockOperation { operation in
|
||||
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
|
||||
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in
|
||||
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
if let image = response?.image {
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completionHandler(image, nil)
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { cell, image, _, error in
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.isIndicatingActivity = false
|
||||
cell.imageView.image = image
|
||||
|
||||
if let error = error {
|
||||
os_log("Error loading image: %@", type: .error, error.localizedDescription)
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission> {
|
||||
let dataSource = RSTArrayCollectionViewDataSource(items: app.permissions)
|
||||
dataSource.cellConfigurationHandler = { cell, permission, _ in
|
||||
|
||||
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions)
|
||||
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
||||
let cell = cell as! PermissionCollectionViewCell
|
||||
cell.button.setImage(permission.type.icon, for: .normal)
|
||||
cell.button.tintColor = .label
|
||||
cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
|
||||
}
|
||||
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppContentViewController {
|
||||
@objc func toggleCollapsingSection(_ sender: UIButton) {
|
||||
private extension AppContentViewController
|
||||
{
|
||||
@objc func toggleCollapsingSection(_ sender: UIButton)
|
||||
{
|
||||
let indexPath: IndexPath
|
||||
|
||||
switch sender {
|
||||
case descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||
case versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||
|
||||
switch sender
|
||||
{
|
||||
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||
default: return
|
||||
}
|
||||
|
||||
|
||||
// Disable animations to prevent some potentially strange ones.
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.reloadRows(at: [indexPath], with: .none)
|
||||
@@ -197,29 +212,35 @@ private extension AppContentViewController {
|
||||
}
|
||||
}
|
||||
|
||||
extension AppContentViewController {
|
||||
override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt _: IndexPath) {
|
||||
cell.tintColor = app.tintColor
|
||||
extension AppContentViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
||||
{
|
||||
cell.tintColor = self.app.tintColor
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
switch Row.allCases[indexPath.row] {
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||
{
|
||||
switch Row.allCases[indexPath.row]
|
||||
{
|
||||
case .screenshots:
|
||||
guard let size = preferredScreenshotSize else { return 0.0 }
|
||||
guard let size = self.preferredScreenshotSize else { return 0.0 }
|
||||
return size.height
|
||||
|
||||
|
||||
case .permissions:
|
||||
guard !app.permissions.isEmpty else { return 0.0 }
|
||||
guard !self.app.permissions.isEmpty else { return 0.0 }
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppContentViewController: UIPopoverPresentationControllerDelegate {
|
||||
func adaptivePresentationStyle(for _: UIPresentationController, traitCollection _: UITraitCollection) -> UIModalPresentationStyle {
|
||||
.none
|
||||
extension AppContentViewController: UIPopoverPresentationControllerDelegate
|
||||
{
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
|
||||
{
|
||||
return .none
|
||||
}
|
||||
}
|
||||
@@ -8,33 +8,36 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
final class PermissionCollectionViewCell: UICollectionViewCell {
|
||||
final class PermissionCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
@IBOutlet var button: UIButton!
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
|
||||
override func layoutSubviews() {
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
button.layer.cornerRadius = button.bounds.midY
|
||||
|
||||
self.button.layer.cornerRadius = self.button.bounds.midY
|
||||
}
|
||||
|
||||
override func tintColorDidChange() {
|
||||
|
||||
override func tintColorDidChange()
|
||||
{
|
||||
super.tintColorDidChange()
|
||||
|
||||
button.backgroundColor = tintColor.withAlphaComponent(0.15)
|
||||
textLabel.textColor = tintColor
|
||||
|
||||
self.button.backgroundColor = self.tintColor.withAlphaComponent(0.15)
|
||||
self.textLabel.textColor = self.tintColor
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
final class AppContentTableViewCell: UITableViewCell {
|
||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
|
||||
final class AppContentTableViewCell: UITableViewCell
|
||||
{
|
||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||
{
|
||||
// Ensure cell is laid out so it will report correct size.
|
||||
layoutIfNeeded()
|
||||
|
||||
self.layoutIfNeeded()
|
||||
|
||||
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
||||
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
570
AltStore/App Detail/AppViewController.swift
Normal file
570
AltStore/App Detail/AppViewController.swift
Normal file
@@ -0,0 +1,570 @@
|
||||
//
|
||||
// AppViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/22/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
final class AppViewController: UIViewController
|
||||
{
|
||||
var app: StoreApp!
|
||||
|
||||
private var contentViewController: AppContentViewController!
|
||||
private var contentViewControllerShadowView: UIView!
|
||||
|
||||
private var blurAnimator: UIViewPropertyAnimator?
|
||||
private var navigationBarAnimator: UIViewPropertyAnimator?
|
||||
|
||||
private var contentSizeObservation: NSKeyValueObservation?
|
||||
|
||||
@IBOutlet private var scrollView: UIScrollView!
|
||||
@IBOutlet private var contentView: UIView!
|
||||
|
||||
@IBOutlet private var bannerView: AppBannerView!
|
||||
|
||||
@IBOutlet private var backButton: UIButton!
|
||||
@IBOutlet private var backButtonContainerView: UIVisualEffectView!
|
||||
|
||||
@IBOutlet private var backgroundAppIconImageView: UIImageView!
|
||||
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
|
||||
|
||||
@IBOutlet private var navigationBarTitleView: UIView!
|
||||
@IBOutlet private var navigationBarDownloadButton: PillButton!
|
||||
@IBOutlet private var navigationBarAppIconImageView: UIImageView!
|
||||
@IBOutlet private var navigationBarAppNameLabel: UILabel!
|
||||
|
||||
private var _shouldResetLayout = false
|
||||
private var _backgroundBlurEffect: UIBlurEffect?
|
||||
private var _backgroundBlurTintColor: UIColor?
|
||||
|
||||
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return _preferredStatusBarStyle
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.navigationBarTitleView.sizeToFit()
|
||||
self.navigationItem.titleView = self.navigationBarTitleView
|
||||
|
||||
self.contentViewControllerShadowView = UIView()
|
||||
self.contentViewControllerShadowView.backgroundColor = .white
|
||||
self.contentViewControllerShadowView.layer.cornerRadius = 38
|
||||
self.contentViewControllerShadowView.layer.shadowColor = UIColor.black.cgColor
|
||||
self.contentViewControllerShadowView.layer.shadowOffset = CGSize(width: 0, height: -1)
|
||||
self.contentViewControllerShadowView.layer.shadowRadius = 10
|
||||
self.contentViewControllerShadowView.layer.shadowOpacity = 0.3
|
||||
self.contentViewController.view.superview?.insertSubview(self.contentViewControllerShadowView, at: 0)
|
||||
|
||||
self.contentView.addGestureRecognizer(self.scrollView.panGestureRecognizer)
|
||||
|
||||
self.contentViewController.view.layer.cornerRadius = 38
|
||||
self.contentViewController.view.layer.masksToBounds = true
|
||||
|
||||
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
|
||||
self.contentViewController.tableView.showsVerticalScrollIndicator = false
|
||||
|
||||
// Bring to front so the scroll indicators are visible.
|
||||
self.view.bringSubviewToFront(self.scrollView)
|
||||
self.scrollView.isUserInteractionEnabled = false
|
||||
|
||||
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
|
||||
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
|
||||
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
||||
self.bannerView.iconImageView.image = nil
|
||||
self.bannerView.iconImageView.tintColor = self.app.tintColor
|
||||
self.bannerView.button.tintColor = self.app.tintColor
|
||||
self.bannerView.tintColor = self.app.tintColor
|
||||
|
||||
self.bannerView.configure(for: self.app)
|
||||
self.bannerView.accessibilityTraits.remove(.button)
|
||||
|
||||
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
|
||||
self.backButtonContainerView.tintColor = self.app.tintColor
|
||||
|
||||
self.navigationController?.navigationBar.tintColor = self.app.tintColor
|
||||
self.navigationBarDownloadButton.tintColor = self.app.tintColor
|
||||
self.navigationBarAppNameLabel.text = self.app.name
|
||||
self.navigationBarAppIconImageView.tintColor = self.app.tintColor
|
||||
|
||||
self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { [weak self] (tableView, change) in
|
||||
self?.view.setNeedsLayout()
|
||||
self?.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
self.update()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
|
||||
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
|
||||
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
|
||||
|
||||
// Load Images
|
||||
for imageView in [self.bannerView.iconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
|
||||
{
|
||||
imageView.isIndicatingActivity = true
|
||||
|
||||
Nuke.loadImage(with: self.app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] (response, error) in
|
||||
if response?.image != nil
|
||||
{
|
||||
imageView?.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.prepareBlur()
|
||||
|
||||
// Update blur immediately.
|
||||
self.view.setNeedsLayout()
|
||||
self.view.layoutIfNeeded()
|
||||
|
||||
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
||||
self.hideNavigationBar()
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self._shouldResetLayout = true
|
||||
self.view.setNeedsLayout()
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Guard against "dismissing" when presenting via 3D Touch pop.
|
||||
guard self.navigationController != nil else { return }
|
||||
|
||||
// Store reference since self.navigationController will be nil after disappearing.
|
||||
let navigationController = self.navigationController
|
||||
navigationController?.navigationBar.barStyle = .default // Don't animate, or else status bar might appear messed-up.
|
||||
|
||||
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
||||
self.showNavigationBar(for: navigationController)
|
||||
}, completion: { (context) in
|
||||
if !context.isCancelled
|
||||
{
|
||||
self.showNavigationBar(for: navigationController)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
if self.navigationController == nil
|
||||
{
|
||||
self.resetNavigationBarAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
{
|
||||
guard segue.identifier == "embedAppContentViewController" else { return }
|
||||
|
||||
self.contentViewController = segue.destination as? AppContentViewController
|
||||
self.contentViewController.app = self.app
|
||||
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
// Fix navigation bar + tab bar appearance on iOS 15.
|
||||
self.setContentScrollView(self.scrollView)
|
||||
self.navigationItem.scrollEdgeAppearance = self.navigationController?.navigationBar.standardAppearance
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
if self._shouldResetLayout
|
||||
{
|
||||
// Various events can cause UI to mess up, so reset affected components now.
|
||||
|
||||
if self.navigationController?.topViewController == self
|
||||
{
|
||||
self.hideNavigationBar()
|
||||
}
|
||||
|
||||
self.prepareBlur()
|
||||
|
||||
// Reset navigation bar animation, and create a new one later in this method if necessary.
|
||||
self.resetNavigationBarAnimation()
|
||||
|
||||
self._shouldResetLayout = false
|
||||
}
|
||||
|
||||
let statusBarHeight = UIApplication.shared.statusBarFrame.height
|
||||
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.scrollIndicatorInsets.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.latestVersion?.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)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.bannerView.button.progress = nil
|
||||
self.navigationBarDownloadButton.progress = nil
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
self.bannerView.button.progress = group.progress
|
||||
self.navigationBarDownloadButton.progress = group.progress
|
||||
}
|
||||
|
||||
func open(_ installedApp: InstalledApp)
|
||||
{
|
||||
UIApplication.shared.open(installedApp.openAppURL)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppViewController
|
||||
{
|
||||
@objc func didChangeApp(_ notification: Notification)
|
||||
{
|
||||
// Async so that AppManager.installationProgress(for:) is nil when we update.
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func willEnterForeground(_ notification: Notification)
|
||||
{
|
||||
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
||||
|
||||
self._shouldResetLayout = true
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
|
||||
@objc func didBecomeActive(_ notification: Notification)
|
||||
{
|
||||
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
||||
|
||||
// Fixes Navigation Bar appearing after app becomes inactive -> active again.
|
||||
self._shouldResetLayout = true
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppViewController: UIScrollViewDelegate
|
||||
{
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView)
|
||||
{
|
||||
self.view.setNeedsLayout()
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,20 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import SideStoreCore
|
||||
import AltStoreCore
|
||||
|
||||
final class PermissionPopoverViewController: UIViewController {
|
||||
final class PermissionPopoverViewController: UIViewController
|
||||
{
|
||||
var permission: AppPermission!
|
||||
|
||||
|
||||
@IBOutlet private var nameLabel: UILabel!
|
||||
@IBOutlet private var descriptionLabel: UILabel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
nameLabel.text = permission.type.localizedName
|
||||
descriptionLabel.text = permission.usageDescription
|
||||
|
||||
self.nameLabel.text = self.permission.type.localizedName
|
||||
self.descriptionLabel.text = self.permission.usageDescription
|
||||
}
|
||||
}
|
||||
241
AltStore/App IDs/AppIDsViewController.swift
Normal file
241
AltStore/App IDs/AppIDsViewController.swift
Normal file
@@ -0,0 +1,241 @@
|
||||
//
|
||||
// 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 numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
|
||||
let numberOfDaysText = (numberOfDays == 1) ? NSLocalizedString("1 day", comment: "") : String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
|
||||
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
|
||||
|
||||
attributedAccessibilityLabel.mutableString.append(String(format: NSLocalizedString("Expires in %@.", comment: ""), numberOfDaysText) + " ")
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.bannerView.button.isHidden = true
|
||||
cell.bannerView.button.isUserInteractionEnabled = true
|
||||
|
||||
cell.bannerView.buttonLabel.isHidden = true
|
||||
}
|
||||
|
||||
cell.bannerView.titleLabel.text = appID.name
|
||||
cell.bannerView.subtitleLabel.text = appID.bundleIdentifier
|
||||
cell.bannerView.subtitleLabel.numberOfLines = 2
|
||||
|
||||
let attributedBundleIdentifier = NSMutableAttributedString(string: appID.bundleIdentifier.lowercased(), attributes: [.accessibilitySpeechPunctuation: true])
|
||||
|
||||
if let team = appID.team, let range = attributedBundleIdentifier.string.range(of: team.identifier.lowercased()), #available(iOS 13, *)
|
||||
{
|
||||
// Prefer to speak the team ID one character at a time.
|
||||
let nsRange = NSRange(range, in: attributedBundleIdentifier.string)
|
||||
attributedBundleIdentifier.addAttributes([.accessibilitySpeechSpellOut: true], range: nsRange)
|
||||
}
|
||||
|
||||
attributedAccessibilityLabel.append(attributedBundleIdentifier)
|
||||
cell.bannerView.accessibilityAttributedLabel = attributedAccessibilityLabel
|
||||
|
||||
// Make sure refresh button is correct size.
|
||||
cell.layoutIfNeeded()
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
@objc func fetchAppIDs()
|
||||
{
|
||||
guard !self.isLoading else { return }
|
||||
self.isLoading = true
|
||||
|
||||
AppManager.shared.fetchAppIDs { (result) in
|
||||
do
|
||||
{
|
||||
let (_, context) = try result.get()
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
if !self.isLoading
|
||||
{
|
||||
self.collectionView.refreshControl?.endRefreshing()
|
||||
self.activityIndicatorBarButtonItem.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
||||
{
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||
{
|
||||
return CGSize(width: collectionView.bounds.width, height: 80)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
|
||||
{
|
||||
let indexPath = IndexPath(row: 0, section: section)
|
||||
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
|
||||
|
||||
// Use this view to calculate the optimal size based on the collection view's width
|
||||
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
|
||||
withHorizontalFittingPriority: .required, // Width is fixed
|
||||
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
|
||||
return size
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||
{
|
||||
return CGSize(width: collectionView.bounds.width, height: 50)
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||
{
|
||||
switch kind
|
||||
{
|
||||
case UICollectionView.elementKindSectionHeader:
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! TextCollectionReusableView
|
||||
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
if let activeTeam = DatabaseManager.shared.activeTeam(), activeTeam.type == .free
|
||||
{
|
||||
let text = NSLocalizedString("""
|
||||
Each app and app extension installed with SideStore must register an App ID with Apple. Apple limits non-developer Apple IDs to 10 App IDs at a time.
|
||||
|
||||
**App IDs can't be deleted**, but they do expire after one week. SideStore will automatically renew App IDs for all active apps once they've expired.
|
||||
""", comment: "")
|
||||
|
||||
let attributedText = NSAttributedString(markdownRepresentation: text, attributes: [.font: headerView.textLabel.font as Any])
|
||||
headerView.textLabel.attributedText = attributedText
|
||||
}
|
||||
else
|
||||
{
|
||||
headerView.textLabel.text = NSLocalizedString("""
|
||||
Each app and app extension installed with SideStore must register an App ID with Apple.
|
||||
|
||||
App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
|
||||
""", comment: "")
|
||||
}
|
||||
|
||||
return headerView
|
||||
|
||||
case UICollectionView.elementKindSectionFooter:
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! TextCollectionReusableView
|
||||
|
||||
let count = self.dataSource.itemCount
|
||||
if count == 1
|
||||
{
|
||||
footerView.textLabel.text = NSLocalizedString("1 App ID", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs", comment: ""), NSNumber(value: count))
|
||||
}
|
||||
|
||||
return footerView
|
||||
|
||||
default: fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,162 +6,192 @@
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Intents
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
import AVFoundation
|
||||
import Intents
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import SideStoreCore
|
||||
import SideStoreAppKit
|
||||
import Roxas
|
||||
import EmotionalDamage
|
||||
import RoxasUIKit
|
||||
|
||||
extension AppDelegate
|
||||
{
|
||||
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
|
||||
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
|
||||
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
|
||||
|
||||
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
|
||||
|
||||
static let importAppDeepLinkURLKey = "fileURL"
|
||||
static let appBackupResultKey = "result"
|
||||
static let addSourceDeepLinkURLKey = "sourceURL"
|
||||
}
|
||||
|
||||
@UIApplicationMain
|
||||
final class AppDelegate: SideStoreAppDelegate {
|
||||
var window: UIWindow?
|
||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
@available(iOS 14, *)
|
||||
private var intentHandler: IntentHandler {
|
||||
get { _intentHandler as! IntentHandler }
|
||||
set { _intentHandler = newValue }
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 14, *)
|
||||
private var viewAppIntentHandler: ViewAppIntentHandler {
|
||||
get { _viewAppIntentHandler as! ViewAppIntentHandler }
|
||||
set { _viewAppIntentHandler = newValue }
|
||||
}
|
||||
|
||||
|
||||
private lazy var _intentHandler: Any = {
|
||||
guard #available(iOS 14, *) else { fatalError() }
|
||||
return IntentHandler()
|
||||
}()
|
||||
|
||||
|
||||
private lazy var _viewAppIntentHandler: Any = {
|
||||
guard #available(iOS 14, *) else { fatalError() }
|
||||
return ViewAppIntentHandler()
|
||||
}()
|
||||
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
// Register default settings before doing anything else.
|
||||
UserDefaults.registerDefaults()
|
||||
|
||||
DatabaseManager.shared.start { error in
|
||||
if let error = error {
|
||||
os_log("Failed to start DatabaseManager. Error: %@", type: .error , error.localizedDescription)
|
||||
} else {
|
||||
os_log("Started DatabaseManager.", type: .info)
|
||||
let transformer = ALTAppPermissionTypeTransformer()
|
||||
ValueTransformer.setValueTransformer(transformer, forName: NSValueTransformerName(rawValue: "ALTAppPermissionTypeTransformer"))
|
||||
|
||||
DatabaseManager.shared.start { (error) in
|
||||
if let error = error
|
||||
{
|
||||
print("Failed to start DatabaseManager. Error:", error as Any)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Started DatabaseManager.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AnalyticsManager.shared.start()
|
||||
|
||||
setTintColor()
|
||||
|
||||
SecureValueTransformer.register()
|
||||
|
||||
if UserDefaults.standard.firstLaunch == nil {
|
||||
|
||||
self.setTintColor()
|
||||
|
||||
SecureValueTransformer.register()
|
||||
|
||||
if UserDefaults.standard.firstLaunch == nil
|
||||
{
|
||||
Keychain.shared.reset()
|
||||
UserDefaults.standard.firstLaunch = Date()
|
||||
}
|
||||
|
||||
|
||||
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
||||
|
||||
|
||||
#if DEBUG || BETA
|
||||
UserDefaults.standard.isDebugModeEnabled = true
|
||||
UserDefaults.standard.isDebugModeEnabled = true
|
||||
#endif
|
||||
|
||||
prepareForBackgroundFetch()
|
||||
|
||||
|
||||
self.prepareForBackgroundFetch()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_: UIApplication) {
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication)
|
||||
{
|
||||
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
||||
|
||||
|
||||
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||
|
||||
|
||||
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
|
||||
switch result {
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case let .failure(error): os_log("[ALTLog] Failed to purge logged errors before %@. %@", type: .error , midnightOneMonthAgo.debugDescription, error.localizedDescription)
|
||||
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_: UIApplication) {
|
||||
func applicationWillEnterForeground(_ application: UIApplication)
|
||||
{
|
||||
AppManager.shared.update()
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
}
|
||||
|
||||
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
||||
open(url)
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||
{
|
||||
return self.open(url)
|
||||
}
|
||||
|
||||
func application(_: UIApplication, handlerFor intent: INIntent) -> Any? {
|
||||
|
||||
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
|
||||
{
|
||||
guard #available(iOS 14, *) else { return nil }
|
||||
|
||||
switch intent {
|
||||
case is RefreshAllIntent: return intentHandler
|
||||
case is ViewAppIntent: return viewAppIntentHandler
|
||||
|
||||
switch intent
|
||||
{
|
||||
case is RefreshAllIntent: return self.intentHandler
|
||||
case is ViewAppIntent: return self.viewAppIntentHandler
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
extension AppDelegate {
|
||||
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
extension AppDelegate
|
||||
{
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
|
||||
{
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_: UIApplication, didDiscardSceneSessions _: Set<UISceneSession>) {
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
|
||||
{
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate {
|
||||
func setTintColor() {
|
||||
window?.tintColor = .altPrimary
|
||||
private extension AppDelegate
|
||||
{
|
||||
func setTintColor()
|
||||
{
|
||||
self.window?.tintColor = .altPrimary
|
||||
}
|
||||
|
||||
func open(_ url: URL) -> Bool {
|
||||
if url.isFileURL {
|
||||
|
||||
func open(_ url: URL) -> Bool
|
||||
{
|
||||
if url.isFileURL
|
||||
{
|
||||
guard url.pathExtension.lowercased() == "ipa" else { return false }
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||
guard let host = components.host?.lowercased() else { return false }
|
||||
|
||||
switch host {
|
||||
|
||||
switch host
|
||||
{
|
||||
case "patreon":
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
case "appbackupresponse":
|
||||
let result: Result<Void, Error>
|
||||
|
||||
switch url.path.lowercased() {
|
||||
|
||||
switch url.path.lowercased()
|
||||
{
|
||||
case "/success": result = .success(())
|
||||
case "/failure":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
|
||||
@@ -170,193 +200,216 @@ private extension AppDelegate {
|
||||
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
|
||||
let errorDescription = queryItems["errorDescription"]
|
||||
else { return false }
|
||||
|
||||
|
||||
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
||||
result = .failure(error)
|
||||
|
||||
|
||||
default: return false
|
||||
}
|
||||
|
||||
|
||||
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
case "install":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
case "source":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate {
|
||||
private func prepareForBackgroundFetch() {
|
||||
extension AppDelegate
|
||||
{
|
||||
private func prepareForBackgroundFetch()
|
||||
{
|
||||
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
|
||||
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
#endif
|
||||
}
|
||||
|
||||
func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||
{
|
||||
let tokenParts = deviceToken.map { data -> String in
|
||||
String(format: "%02.2hhx", data)
|
||||
return String(format: "%02.2hhx", data)
|
||||
}
|
||||
|
||||
|
||||
let token = tokenParts.joined()
|
||||
os_log("Push Token: %@", type: .debug , token)
|
||||
print("Push Token:", token)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
|
||||
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||
{
|
||||
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func application(_: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification {
|
||||
|
||||
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||
{
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
|
||||
{
|
||||
let threeHours: TimeInterval = 3 * 60 * 60
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
||||
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
||||
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
|
||||
|
||||
|
||||
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
|
||||
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||
}
|
||||
|
||||
BackgroundTaskManager.shared.performExtendedBackgroundTask { taskResult, taskCompletionHandler in
|
||||
if let error = taskResult.error {
|
||||
os_log("Error starting extended background task. Aborting. %@", type: .error, error.localizedDescription)
|
||||
|
||||
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
||||
if let error = taskResult.error
|
||||
{
|
||||
print("Error starting extended background task. Aborting.", error)
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
taskCompletionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
if !DatabaseManager.shared.isStarted {
|
||||
DatabaseManager.shared.start { error in
|
||||
if error != nil {
|
||||
|
||||
if !DatabaseManager.shared.isStarted
|
||||
{
|
||||
DatabaseManager.shared.start() { (error) in
|
||||
if error != nil
|
||||
{
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
taskCompletionHandler()
|
||||
} else {
|
||||
self.performBackgroundFetch { backgroundFetchResult in
|
||||
}
|
||||
else
|
||||
{
|
||||
self.performBackgroundFetch { (backgroundFetchResult) in
|
||||
backgroundFetchCompletionHandler(backgroundFetchResult)
|
||||
} refreshAppsCompletionHandler: { _ in
|
||||
} refreshAppsCompletionHandler: { (refreshAppsResult) in
|
||||
taskCompletionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.performBackgroundFetch { backgroundFetchResult in
|
||||
}
|
||||
else
|
||||
{
|
||||
self.performBackgroundFetch { (backgroundFetchResult) in
|
||||
backgroundFetchCompletionHandler(backgroundFetchResult)
|
||||
} refreshAppsCompletionHandler: { _ in
|
||||
} refreshAppsCompletionHandler: { (refreshAppsResult) in
|
||||
taskCompletionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) {
|
||||
fetchSources { result in
|
||||
switch result {
|
||||
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||
{
|
||||
self.fetchSources { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||
case .success: backgroundFetchCompletionHandler(.newData)
|
||||
}
|
||||
|
||||
if !UserDefaults.standard.isBackgroundRefreshEnabled {
|
||||
|
||||
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
refreshAppsCompletionHandler(.success([:]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate {
|
||||
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void) {
|
||||
AppManager.shared.fetchSources { result in
|
||||
do {
|
||||
private extension AppDelegate
|
||||
{
|
||||
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.fetchSources() { (result) in
|
||||
do
|
||||
{
|
||||
let (sources, context) = try result.get()
|
||||
|
||||
|
||||
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||
previousUpdatesFetchRequest.resultType = .dictionaryResultType
|
||||
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
|
||||
|
||||
|
||||
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
|
||||
previousNewsItemsFetchRequest.includesPendingChanges = false
|
||||
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
|
||||
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
|
||||
|
||||
|
||||
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
|
||||
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
|
||||
|
||||
|
||||
try context.save()
|
||||
|
||||
|
||||
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
|
||||
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||
|
||||
|
||||
let updates = try context.fetch(updatesFetchRequest)
|
||||
let newsItems = try context.fetch(newsItemsFetchRequest)
|
||||
|
||||
for update in updates {
|
||||
|
||||
for update in updates
|
||||
{
|
||||
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
||||
guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
|
||||
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("New Update Available", comment: "")
|
||||
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
|
||||
content.sound = .default
|
||||
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
|
||||
for newsItem in newsItems {
|
||||
|
||||
for newsItem in newsItems
|
||||
{
|
||||
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
|
||||
guard !newsItem.isSilent else { continue }
|
||||
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
if let app = newsItem.storeApp {
|
||||
|
||||
if let app = newsItem.storeApp
|
||||
{
|
||||
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
content.title = NSLocalizedString("SideStore News", comment: "")
|
||||
}
|
||||
|
||||
|
||||
content.body = newsItem.title
|
||||
content.sound = .default
|
||||
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
@@ -364,10 +417,12 @@ private extension AppDelegate {
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.applicationIconBadgeNumber = updates.count
|
||||
}
|
||||
|
||||
|
||||
completionHandler(.success(sources))
|
||||
} catch {
|
||||
os_log("Error fetching apps: %@", type: .error, error.localizedDescription)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error fetching apps:", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -12,9 +12,9 @@
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="lNR-II-WoW">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="navigationController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="ZTo-53-dSL" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="59" width="393" height="96"/>
|
||||
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="barTintColor" name="SettingsBackground"/>
|
||||
@@ -36,34 +36,34 @@
|
||||
<!--Authentication View Controller-->
|
||||
<scene sceneID="OCd-xc-Ms7">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="authenticationViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
|
||||
<rect key="frame" x="0.0" y="103" width="393" height="715"/>
|
||||
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
|
||||
</view>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="715"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
|
||||
<rect key="frame" x="16" y="6" width="361" height="359"/>
|
||||
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
|
||||
<rect key="frame" x="0.0" y="0.0" width="361" height="67"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="357.33333333333331" height="40.666666666666664"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="332" height="41"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
|
||||
<rect key="frame" x="0.0" y="46.666666666666664" width="306.33333333333331" height="20.333333333333336"/>
|
||||
<rect key="frame" x="0.0" y="47" width="306.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -71,19 +71,19 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf">
|
||||
<rect key="frame" x="0.0" y="117" width="361" height="242"/>
|
||||
<rect key="frame" x="0.0" y="117.5" width="343" height="242"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="361" height="159"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="159"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID">
|
||||
<rect key="frame" x="0.0" y="0.0" width="361" height="72"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="72"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="APPLE ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="59N-O1-6bM">
|
||||
<rect key="frame" x="14" y="0.0" width="347" height="17"/>
|
||||
<rect key="frame" x="14" y="0.0" width="329" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -92,10 +92,10 @@
|
||||
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
|
||||
</stackView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1">
|
||||
<rect key="frame" x="0.0" y="21" width="361" height="51"/>
|
||||
<rect key="frame" x="0.0" y="21" width="343" height="51"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo">
|
||||
<rect key="frame" x="14" y="0.0" width="333" height="51"/>
|
||||
<rect key="frame" x="14" y="0.0" width="315" height="51"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
||||
@@ -118,13 +118,13 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password">
|
||||
<rect key="frame" x="0.0" y="87" width="361" height="72"/>
|
||||
<rect key="frame" x="0.0" y="87" width="343" height="72"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95">
|
||||
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PASSWORD" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ava-XY-7vs">
|
||||
<rect key="frame" x="14" y="0.0" width="347" height="17"/>
|
||||
<rect key="frame" x="14" y="0.0" width="329" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -133,10 +133,10 @@
|
||||
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
|
||||
</stackView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5">
|
||||
<rect key="frame" x="0.0" y="21" width="361" height="51"/>
|
||||
<rect key="frame" x="0.0" y="21" width="343" height="51"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT">
|
||||
<rect key="frame" x="14" y="0.0" width="333" height="51"/>
|
||||
<rect key="frame" x="14" y="0.0" width="315" height="51"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
||||
@@ -161,7 +161,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
|
||||
<rect key="frame" x="0.0" y="191" width="361" height="51"/>
|
||||
<rect key="frame" x="0.0" y="191" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
|
||||
@@ -179,16 +179,16 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
|
||||
<rect key="frame" x="16" y="611" width="361" height="96"/>
|
||||
<rect key="frame" x="16" y="518.5" width="343" height="96.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="361" height="20.333333333333332"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY">
|
||||
<rect key="frame" x="0.0" y="24.333333333333371" width="361" height="71.666666666666671"/>
|
||||
<rect key="frame" x="0.0" y="24.5" width="343" height="72"/>
|
||||
<string key="text">Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -258,19 +258,19 @@
|
||||
<!--How it works-->
|
||||
<scene sceneID="dMt-EA-SGy">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
|
||||
<rect key="frame" x="0.0" y="103" width="393" height="656"/>
|
||||
<rect key="frame" x="0.0" y="44" width="375" height="564"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
|
||||
<rect key="frame" x="16" y="35.000000000000007" width="361" height="95.666666666666686"/>
|
||||
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-3h-B8f">
|
||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/>
|
||||
</constraints>
|
||||
@@ -279,16 +279,16 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0">
|
||||
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/>
|
||||
<rect key="frame" x="79" y="16" width="264" height="64"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0">
|
||||
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH">
|
||||
<rect key="frame" x="0.0" y="25.333333333333346" width="282" height="38.333333333333343"/>
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -298,10 +298,10 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
|
||||
<rect key="frame" x="16" y="198.33333333333331" width="361" height="95.666666666666686"/>
|
||||
<rect key="frame" x="16" y="168" width="343" height="95.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/>
|
||||
</constraints>
|
||||
@@ -310,16 +310,16 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
|
||||
<rect key="frame" x="79" y="16.333333333333371" width="282" height="63"/>
|
||||
<rect key="frame" x="79" y="17" width="264" height="61.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
|
||||
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
|
||||
<rect key="frame" x="0.0" y="25.333333333333311" width="282" height="37.666666666666657"/>
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="36"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -329,10 +329,10 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
|
||||
<rect key="frame" x="16" y="362" width="361" height="95.666666666666686"/>
|
||||
<rect key="frame" x="16" y="300.5" width="343" height="95.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
|
||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/>
|
||||
</constraints>
|
||||
@@ -341,16 +341,16 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
|
||||
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/>
|
||||
<rect key="frame" x="79" y="16" width="264" height="64"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
|
||||
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt">
|
||||
<rect key="frame" x="0.0" y="25.333333333333318" width="282" height="38.333333333333343"/>
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -360,10 +360,10 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
|
||||
<rect key="frame" x="16" y="525.33333333333337" width="361" height="95.666666666666629"/>
|
||||
<rect key="frame" x="16" y="433.5" width="343" height="95.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
|
||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/>
|
||||
</constraints>
|
||||
@@ -372,16 +372,16 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
|
||||
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/>
|
||||
<rect key="frame" x="79" y="17" width="264" height="62"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
|
||||
<rect key="frame" x="0.0" y="25.333333333333261" width="282" height="38.333333333333343"/>
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="36.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -394,7 +394,7 @@
|
||||
<edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
|
||||
<rect key="frame" x="16" y="759" width="361" height="51"/>
|
||||
<rect key="frame" x="16" y="608" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
|
||||
@@ -431,22 +431,22 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1353" y="736"/>
|
||||
</scene>
|
||||
<!--Refresh SideStore-->
|
||||
<!--Refresh AltStore-->
|
||||
<scene sceneID="9Vh-dM-OqX">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="refreshAltStoreViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
|
||||
<rect key="frame" x="16" y="721" width="361" height="89"/>
|
||||
<rect key="frame" x="16" y="570" width="343" height="89"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="361" height="51"/>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/>
|
||||
@@ -461,7 +461,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
|
||||
<rect key="frame" x="0.0" y="59" width="361" height="30"/>
|
||||
<rect key="frame" x="0.0" y="59" width="343" height="30"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" title="Refresh Later">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -485,7 +485,7 @@
|
||||
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="Refresh SideStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
|
||||
<navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<connections>
|
||||
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
|
||||
@@ -498,30 +498,30 @@
|
||||
<!--Select a Team-->
|
||||
<scene sceneID="ioQ-WB-CLJ">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="55.333332061767578" width="393" height="60"/>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I">
|
||||
<rect key="frame" x="0.0" y="0.0" width="352.66666666666669" height="60"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="334" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM">
|
||||
<rect key="frame" x="30.000000000000004" y="9.9999999999999982" width="56.333333333333336" height="20.333333333333332"/>
|
||||
<rect key="frame" x="30" y="10" width="56.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf">
|
||||
<rect key="frame" x="30" y="33.333333333333329" width="70" height="14.333333333333334"/>
|
||||
<rect key="frame" x="30" y="33.5" width="70" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -551,18 +551,19 @@
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1401" y="734"/>
|
||||
|
||||
</scene>
|
||||
</scenes>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
<resources>
|
||||
<namedColor name="Primary">
|
||||
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</scenes>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
<resources>
|
||||
<namedColor name="Primary">
|
||||
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="SettingsBackground">
|
||||
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="SettingsHighlighted">
|
||||
<color red="0.38823529411764707" green="0.011764705882352941" blue="0.58823529411764708" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
171
AltStore/Authentication/AuthenticationViewController.swift
Normal file
171
AltStore/Authentication/AuthenticationViewController.swift
Normal file
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// 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.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: ""))
|
||||
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.textLabel.textColor = .altPink
|
||||
toastView.detailTextLabel.textColor = .altPink
|
||||
toastView.show(in: self)
|
||||
self.toastView = toastView
|
||||
|
||||
self.signInButton.isIndicatingActivity = false
|
||||
}
|
||||
|
||||
case .success((let account, let session)):
|
||||
self.completionHandler?((account, session, password))
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.scrollView.setContentOffset(CGPoint(x: 0, y: -self.view.safeAreaInsets.top), animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func cancel(_ sender: UIBarButtonItem)
|
||||
{
|
||||
self.completionHandler?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationViewController: UITextFieldDelegate
|
||||
{
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool
|
||||
{
|
||||
switch textField
|
||||
{
|
||||
case self.appleIDTextField: self.passwordTextField.becomeFirstResponder()
|
||||
case self.passwordTextField: self.authenticate()
|
||||
default: break
|
||||
}
|
||||
|
||||
self.update()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField)
|
||||
{
|
||||
guard UIScreen.main.isExtraCompactHeight else { return }
|
||||
|
||||
// Position all the controls within visible frame.
|
||||
var contentOffset = self.scrollView.contentOffset
|
||||
contentOffset.y = 44
|
||||
self.scrollView.setContentOffset(contentOffset, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationViewController
|
||||
{
|
||||
@objc func textFieldDidChangeText(_ notification: Notification)
|
||||
{
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
54
AltStore/Authentication/InstructionsViewController.swift
Normal file
54
AltStore/Authentication/InstructionsViewController.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// InstructionsViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/6/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class InstructionsViewController: UIViewController
|
||||
{
|
||||
var completionHandler: (() -> Void)?
|
||||
|
||||
var showsBottomButton: Bool = false
|
||||
|
||||
@IBOutlet private var contentStackView: UIStackView!
|
||||
@IBOutlet private var dismissButton: UIButton!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
if UIScreen.main.isExtraCompactHeight
|
||||
{
|
||||
self.contentStackView.layoutMargins.top = 0
|
||||
self.contentStackView.layoutMargins.bottom = self.contentStackView.layoutMargins.left
|
||||
}
|
||||
|
||||
self.dismissButton.clipsToBounds = true
|
||||
self.dismissButton.layer.cornerRadius = 16
|
||||
|
||||
if self.showsBottomButton
|
||||
{
|
||||
self.navigationItem.hidesBackButton = true
|
||||
}
|
||||
else
|
||||
{
|
||||
self.dismissButton.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension InstructionsViewController
|
||||
{
|
||||
@IBAction func dismiss()
|
||||
{
|
||||
self.completionHandler?()
|
||||
}
|
||||
}
|
||||
@@ -8,69 +8,77 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import SideStoreCore
|
||||
import RoxasUIKit
|
||||
import Roxas
|
||||
|
||||
final class RefreshAltStoreViewController: UIViewController {
|
||||
final class RefreshAltStoreViewController: UIViewController
|
||||
{
|
||||
var context: AuthenticatedOperationContext!
|
||||
|
||||
|
||||
var completionHandler: ((Result<Void, Error>) -> Void)?
|
||||
|
||||
|
||||
@IBOutlet private var placeholderView: RSTPlaceholderView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
placeholderView.textLabel.isHidden = true
|
||||
|
||||
placeholderView.detailTextLabel.textAlignment = .left
|
||||
placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "")
|
||||
|
||||
self.placeholderView.textLabel.isHidden = true
|
||||
|
||||
self.placeholderView.detailTextLabel.textAlignment = .left
|
||||
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
private extension RefreshAltStoreViewController {
|
||||
@IBAction func refreshAltStore(_ sender: PillButton) {
|
||||
private extension RefreshAltStoreViewController
|
||||
{
|
||||
@IBAction func refreshAltStore(_ sender: PillButton)
|
||||
{
|
||||
guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return }
|
||||
|
||||
func refresh() {
|
||||
|
||||
func refresh()
|
||||
{
|
||||
sender.isIndicatingActivity = true
|
||||
|
||||
if let progress = AppManager.shared.installationProgress(for: altStore) {
|
||||
|
||||
if let progress = AppManager.shared.installationProgress(for: altStore)
|
||||
{
|
||||
// Cancel pending AltStore installation so we can start a new one.
|
||||
progress.cancel()
|
||||
}
|
||||
|
||||
|
||||
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
|
||||
let group = AppManager.shared.install(altStore, presentingViewController: self, context: context) { result in
|
||||
switch result {
|
||||
let group = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: self.completionHandler?(.success(()))
|
||||
case let .failure(error as NSError):
|
||||
case .failure(let error as NSError):
|
||||
DispatchQueue.main.async {
|
||||
sender.progress = nil
|
||||
sender.isIndicatingActivity = false
|
||||
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { _ in
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in
|
||||
refresh()
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { _ in
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { (action) in
|
||||
self.completionHandler?(.failure(error))
|
||||
}))
|
||||
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sender.progress = group.progress
|
||||
}
|
||||
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
@IBAction func cancel(_: UIButton) {
|
||||
completionHandler?(.failure(OperationError.cancelled))
|
||||
|
||||
@IBAction func cancel(_ sender: UIButton)
|
||||
{
|
||||
self.completionHandler?(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
61
AltStore/Authentication/SelectTeamViewController.swift
Normal file
61
AltStore/Authentication/SelectTeamViewController.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// SelectTeamViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Megarushing on 4/26/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import MessageUI
|
||||
import Intents
|
||||
import IntentsUI
|
||||
|
||||
import AltSign
|
||||
|
||||
final class SelectTeamViewController: UITableViewController
|
||||
{
|
||||
public var teams: [ALTTeam]?
|
||||
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?
|
||||
|
||||
private var prototypeHeaderFooterView: SettingsHeaderFooterView!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return teams?.count ?? 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
return self.completionHandler!(.success((self.teams?[indexPath.row])!))
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "TeamCell", for: indexPath) as! InsetGroupTableViewCell
|
||||
|
||||
cell.textLabel?.text = self.teams?[indexPath.row].name
|
||||
cell.detailTextLabel?.text = self.teams?[indexPath.row].type.localizedDescription
|
||||
if indexPath.row == 0
|
||||
{
|
||||
cell.style = InsetGroupTableViewCell.Style.top
|
||||
} else if indexPath.row == self.tableView(self.tableView, numberOfRowsInSection: indexPath.section) - 1 {
|
||||
cell.style = InsetGroupTableViewCell.Style.bottom
|
||||
} else {
|
||||
cell.style = InsetGroupTableViewCell.Style.middle
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
"Teams"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21223" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21204"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
@@ -13,7 +13,7 @@
|
||||
<!--Launch View Controller-->
|
||||
<scene sceneID="q24-yd-v7v">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="launchViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -28,7 +28,7 @@
|
||||
<!--Tab Bar Controller-->
|
||||
<scene sceneID="yl2-sM-qoP">
|
||||
<objects>
|
||||
<tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" useStoryboardIdentifierAsRestorationIdentifier="YES" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
|
||||
<rect key="frame" x="0.0" y="975" width="768" height="49"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
@@ -50,7 +50,7 @@
|
||||
<!--Browse-->
|
||||
<scene sceneID="rXq-UR-qQp">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="browseViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<collectionViewController id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -85,7 +85,7 @@
|
||||
<!--App View Controller-->
|
||||
<scene sceneID="TgT-LO-3Er">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="appViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="appViewController" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="0cR-li-tCB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -131,7 +131,7 @@
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStoreAppKit">
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="37" y="287" width="300" height="93"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</view>
|
||||
@@ -191,7 +191,7 @@
|
||||
</constraints>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS">
|
||||
<barButtonItem key="rightBarButtonItem" style="done" id="FLf-DS-F77">
|
||||
<barButtonItem key="rightBarButtonItem" style="plain" id="FLf-DS-F77">
|
||||
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="287" y="6.5" width="72" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
@@ -229,7 +229,7 @@
|
||||
<!--App-->
|
||||
<scene sceneID="CgX-7h-sRI">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="appContentViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<tableViewController id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -511,7 +511,7 @@ World</string>
|
||||
<!--Permission Popover View Controller-->
|
||||
<scene sceneID="24j-EJ-G4e">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="permissionPopoverViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<viewController id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="217"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -566,7 +566,7 @@ World</string>
|
||||
<!--News-->
|
||||
<scene sceneID="bqw-wB-hyB">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="newsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<collectionViewController id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -592,7 +592,7 @@ World</string>
|
||||
<!--Browse-->
|
||||
<scene sceneID="VHa-uP-bFU">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="forwardingNavigationControllerBrowse" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||
@@ -620,7 +620,7 @@ World</string>
|
||||
<!--My Apps-->
|
||||
<scene sceneID="nhh-BJ-XiT">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="forwardingNavigationControllerNyApps" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
|
||||
<color key="badgeColor" name="Primary"/>
|
||||
</tabBarItem>
|
||||
@@ -641,7 +641,7 @@ World</string>
|
||||
<!--My Apps-->
|
||||
<scene sceneID="EC8-Sf-AF9">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="myAppsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<collectionViewController id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -660,7 +660,7 @@ World</string>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStoreAppKit">
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="0.0" width="359" height="60"/>
|
||||
</view>
|
||||
</subviews>
|
||||
@@ -797,7 +797,7 @@ World</string>
|
||||
<!--App IDs-->
|
||||
<scene sceneID="kvf-US-rRe">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="appIDsViewController" title="App IDs" useStoryboardIdentifierAsRestorationIdentifier="YES" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<collectionViewController title="App IDs" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -816,7 +816,7 @@ World</string>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStoreAppKit">
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="YES"/>
|
||||
@@ -835,7 +835,7 @@ World</string>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStoreAppKit">
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
@@ -856,7 +856,7 @@ World</string>
|
||||
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
|
||||
</connections>
|
||||
</collectionReusableView>
|
||||
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStoreAppKit">
|
||||
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="170" width="375" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
@@ -881,9 +881,9 @@ World</string>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
|
||||
<barButtonItem key="leftBarButtonItem" style="done" id="Aqs-QK-Ups">
|
||||
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
|
||||
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
|
||||
<rect key="frame" x="16" y="7" width="83" height="42"/>
|
||||
<rect key="frame" x="16" y="1" width="83" height="42"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</view>
|
||||
</barButtonItem>
|
||||
@@ -905,7 +905,7 @@ World</string>
|
||||
<!--News-->
|
||||
<scene sceneID="BV8-6J-nIv">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="forwardingNavigationControllerNews" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||
@@ -925,10 +925,10 @@ World</string>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="1Gj-mS-BaN">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="nav1" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="IXk-qg-mFJ" sceneMemberID="viewController">
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
@@ -943,7 +943,7 @@ World</string>
|
||||
<!--Sources-->
|
||||
<scene sceneID="0S1-zn-9KZ">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="sourcesViewController" title="Sources" useStoryboardIdentifierAsRestorationIdentifier="YES" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStoreAppKit" sceneMemberID="viewController">
|
||||
<collectionViewController title="Sources" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -962,7 +962,7 @@ World</string>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStoreAppKit">
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="YES"/>
|
||||
@@ -981,7 +981,7 @@ World</string>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStoreAppKit">
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
@@ -1067,10 +1067,10 @@ World</string>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="6NV-LQ-gKB">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="nav2" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Qo4-72-Hmr" sceneMemberID="viewController">
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
@@ -1095,13 +1095,13 @@ World</string>
|
||||
<image name="News" width="19" height="20"/>
|
||||
<image name="Settings" width="20" height="20"/>
|
||||
<namedColor name="Background">
|
||||
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="BlurTint">
|
||||
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="Primary">
|
||||
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -8,80 +8,85 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import RoxasUIKit
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@objc final class BrowseCollectionViewCell: UICollectionViewCell {
|
||||
@objc final class BrowseCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
var imageURLs: [URL] = [] {
|
||||
didSet {
|
||||
dataSource.items = imageURLs as [NSURL]
|
||||
self.dataSource.items = self.imageURLs as [NSURL]
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
|
||||
@IBOutlet var bannerView: AppBannerView!
|
||||
@IBOutlet var subtitleLabel: UILabel!
|
||||
|
||||
|
||||
@IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
|
||||
|
||||
override func awakeFromNib() {
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷♂️.
|
||||
screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
screenshotsCollectionView.delegate = self
|
||||
screenshotsCollectionView.dataSource = dataSource
|
||||
screenshotsCollectionView.prefetchDataSource = dataSource
|
||||
self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
self.screenshotsCollectionView.delegate = self
|
||||
self.screenshotsCollectionView.dataSource = self.dataSource
|
||||
self.screenshotsCollectionView.prefetchDataSource = self.dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension BrowseCollectionViewCell {
|
||||
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> {
|
||||
private extension BrowseCollectionViewCell
|
||||
{
|
||||
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: [])
|
||||
dataSource.cellConfigurationHandler = { cell, _, _ in
|
||||
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.image = nil
|
||||
cell.imageView.isIndicatingActivity = true
|
||||
}
|
||||
dataSource.prefetchHandler = { imageURL, _, completionHandler in
|
||||
RSTAsyncBlockOperation { operation in
|
||||
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
|
||||
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in
|
||||
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
if let image = response?.image {
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completionHandler(image, nil)
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { cell, image, _, error in
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.isIndicatingActivity = false
|
||||
cell.imageView.image = image
|
||||
|
||||
if let error = error {
|
||||
os_log("Error loading image: %@", type: .error , error.localizedDescription)
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt _: IndexPath) -> CGSize {
|
||||
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout
|
||||
{
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||
{
|
||||
// Assuming 9.0 / 16.0 ratio for now.
|
||||
let aspectRatio: CGFloat = 9.0 / 16.0
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="browseCollectionViewCell" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="SideStoreAppKit">
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
|
||||
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="369"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="SideStoreAppKit">
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
|
||||
</constraints>
|
||||
@@ -62,9 +61,4 @@
|
||||
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -8,67 +8,69 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import SideStoreCore
|
||||
import RoxasUIKit
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
class BrowseViewController: UICollectionViewController {
|
||||
class BrowseViewController: UICollectionViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
||||
|
||||
|
||||
private let prototypeCell = BrowseCollectionViewCell.instantiate(with: BrowseCollectionViewCell.nib!)!
|
||||
|
||||
|
||||
private var loadingState: LoadingState = .loading {
|
||||
didSet {
|
||||
update()
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var cachedItemSizes = [String: CGSize]()
|
||||
|
||||
|
||||
@IBOutlet private var sourcesBarButtonItem: UIBarButtonItem!
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
#if BETA
|
||||
dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
|
||||
navigationItem.searchController = dataSource.searchController
|
||||
self.dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
|
||||
self.navigationItem.searchController = self.dataSource.searchController
|
||||
#endif
|
||||
|
||||
prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
collectionView.dataSource = dataSource
|
||||
collectionView.prefetchDataSource = dataSource
|
||||
|
||||
registerForPreviewing(with: self, sourceView: collectionView)
|
||||
|
||||
update()
|
||||
|
||||
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
self.collectionView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.registerForPreviewing(with: self, sourceView: self.collectionView)
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
fetchSource()
|
||||
updateDataSource()
|
||||
|
||||
update()
|
||||
|
||||
self.fetchSource()
|
||||
self.updateDataSource()
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
@IBAction private func unwindFromSourcesViewController(_: UIStoryboardSegue) {
|
||||
fetchSource()
|
||||
|
||||
@IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
|
||||
{
|
||||
self.fetchSource()
|
||||
}
|
||||
}
|
||||
|
||||
private extension BrowseViewController {
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage> {
|
||||
private extension BrowseViewController
|
||||
{
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
||||
{
|
||||
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
|
||||
@@ -76,46 +78,52 @@ private extension BrowseViewController {
|
||||
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)]
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||
dataSource.cellConfigurationHandler = { cell, app, _ in
|
||||
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
||||
let cell = cell as! BrowseCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
|
||||
cell.subtitleLabel.text = app.subtitle
|
||||
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
|
||||
|
||||
|
||||
cell.bannerView.configure(for: app)
|
||||
|
||||
|
||||
cell.bannerView.iconImageView.image = nil
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
|
||||
|
||||
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
cell.bannerView.button.activityIndicatorView.style = .medium
|
||||
|
||||
|
||||
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
|
||||
// Otherwise, cell reuse can mess up some cached values.
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
|
||||
|
||||
let tintColor = app.tintColor ?? .altPrimary
|
||||
cell.tintColor = tintColor
|
||||
|
||||
if app.installedApp == nil {
|
||||
|
||||
if app.installedApp == nil
|
||||
{
|
||||
let buttonTitle = NSLocalizedString("Free", comment: "")
|
||||
cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
||||
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name)
|
||||
cell.bannerView.button.accessibilityValue = buttonTitle
|
||||
|
||||
|
||||
let progress = AppManager.shared.installationProgress(for: app)
|
||||
cell.bannerView.button.progress = progress
|
||||
|
||||
if let versionDate = app.latestVersion?.date, versionDate > Date() {
|
||||
|
||||
if let versionDate = app.latestVersion?.date, versionDate > Date()
|
||||
{
|
||||
cell.bannerView.button.countdownDate = app.versionDate
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.bannerView.button.countdownDate = nil
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name)
|
||||
cell.bannerView.button.accessibilityValue = nil
|
||||
@@ -123,204 +131,234 @@ private extension BrowseViewController {
|
||||
cell.bannerView.button.countdownDate = nil
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { storeApp, _, completionHandler -> Foundation.Operation? in
|
||||
dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
|
||||
let iconURL = storeApp.iconURL
|
||||
|
||||
return RSTAsyncBlockOperation { operation in
|
||||
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { response, error in
|
||||
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
if let image = response?.image {
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completionHandler(image, nil)
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { cell, image, _, error in
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! BrowseCollectionViewCell
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
cell.bannerView.iconImageView.image = image
|
||||
|
||||
if let error = error {
|
||||
os_log("Error loading image: %@", type: .error , error.localizedDescription)
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
dataSource.placeholderView = placeholderView
|
||||
|
||||
|
||||
dataSource.placeholderView = self.placeholderView
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func updateDataSource() {
|
||||
dataSource.predicate = nil
|
||||
|
||||
func updateDataSource()
|
||||
{
|
||||
self.dataSource.predicate = nil
|
||||
}
|
||||
|
||||
func fetchSource() {
|
||||
loadingState = .loading
|
||||
|
||||
AppManager.shared.fetchSources { result in
|
||||
do {
|
||||
do {
|
||||
|
||||
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 {
|
||||
}
|
||||
catch let error as AppManager.FetchSourcesError
|
||||
{
|
||||
try error.managedObjectContext?.save()
|
||||
throw error
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
if self.dataSource.itemCount > 0 {
|
||||
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 loadingState {
|
||||
|
||||
func update()
|
||||
{
|
||||
switch self.loadingState
|
||||
{
|
||||
case .loading:
|
||||
placeholderView.textLabel.isHidden = true
|
||||
placeholderView.detailTextLabel.isHidden = false
|
||||
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
|
||||
|
||||
placeholderView.activityIndicatorView.startAnimating()
|
||||
|
||||
case let .finished(.failure(error)):
|
||||
placeholderView.textLabel.isHidden = false
|
||||
placeholderView.detailTextLabel.isHidden = false
|
||||
|
||||
placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "")
|
||||
placeholderView.detailTextLabel.text = error.localizedDescription
|
||||
|
||||
placeholderView.activityIndicatorView.stopAnimating()
|
||||
|
||||
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 Apps", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = error.localizedDescription
|
||||
|
||||
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||
|
||||
case .finished(.success):
|
||||
placeholderView.textLabel.isHidden = true
|
||||
placeholderView.detailTextLabel.isHidden = true
|
||||
|
||||
placeholderView.activityIndicatorView.stopAnimating()
|
||||
self.placeholderView.textLabel.isHidden = true
|
||||
self.placeholderView.detailTextLabel.isHidden = true
|
||||
|
||||
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension BrowseViewController {
|
||||
@IBAction func performAppAction(_ sender: PillButton) {
|
||||
let point = collectionView.convert(sender.center, from: sender.superview)
|
||||
guard let indexPath = collectionView.indexPathForItem(at: point) else { return }
|
||||
|
||||
let app = dataSource.item(at: indexPath)
|
||||
|
||||
if let installedApp = app.installedApp {
|
||||
open(installedApp)
|
||||
} else {
|
||||
install(app, at: indexPath)
|
||||
private extension BrowseViewController
|
||||
{
|
||||
@IBAction func performAppAction(_ sender: PillButton)
|
||||
{
|
||||
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
||||
|
||||
let app = self.dataSource.item(at: indexPath)
|
||||
|
||||
if let installedApp = app.installedApp
|
||||
{
|
||||
self.open(installedApp)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.install(app, at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
func install(_ app: StoreApp, at indexPath: IndexPath) {
|
||||
|
||||
func install(_ app: StoreApp, at indexPath: IndexPath)
|
||||
{
|
||||
let previousProgress = AppManager.shared.installationProgress(for: app)
|
||||
guard previousProgress == nil else {
|
||||
previousProgress?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
_ = AppManager.shared.install(app, presentingViewController: self) { result in
|
||||
|
||||
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result {
|
||||
switch result
|
||||
{
|
||||
case .failure(OperationError.cancelled): break // Ignore
|
||||
case let .failure(error):
|
||||
case .failure(let error):
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
|
||||
case .success: os_log("Installed app: %@", type: .info , app.bundleIdentifier)
|
||||
|
||||
case .success: print("Installed app:", app.bundleIdentifier)
|
||||
}
|
||||
|
||||
|
||||
self.collectionView.reloadItems(at: [indexPath])
|
||||
}
|
||||
}
|
||||
|
||||
collectionView.reloadItems(at: [indexPath])
|
||||
|
||||
self.collectionView.reloadItems(at: [indexPath])
|
||||
}
|
||||
|
||||
func open(_ installedApp: InstalledApp) {
|
||||
|
||||
func open(_ installedApp: InstalledApp)
|
||||
{
|
||||
UIApplication.shared.open(installedApp.openAppURL)
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowseViewController: UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let item = dataSource.item(at: indexPath)
|
||||
extension BrowseViewController: UICollectionViewDelegateFlowLayout
|
||||
{
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||
{
|
||||
let item = self.dataSource.item(at: indexPath)
|
||||
|
||||
if let previousSize = cachedItemSizes[item.bundleIdentifier] {
|
||||
if let previousSize = self.cachedItemSizes[item.bundleIdentifier]
|
||||
{
|
||||
return previousSize
|
||||
}
|
||||
|
||||
let maxVisibleScreenshots = 2 as CGFloat
|
||||
let aspectRatio: CGFloat = 16.0 / 9.0
|
||||
|
||||
let layout = prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
|
||||
let layout = self.prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
|
||||
|
||||
dataSource.cellConfigurationHandler(prototypeCell, item, indexPath)
|
||||
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
|
||||
|
||||
let widthConstraint = prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||
widthConstraint.isActive = true
|
||||
defer { widthConstraint.isActive = false }
|
||||
|
||||
// Manually update cell width & layout so we can accurately calculate screenshot sizes.
|
||||
prototypeCell.frame.size.width = widthConstraint.constant
|
||||
prototypeCell.layoutIfNeeded()
|
||||
|
||||
let collectionViewWidth = prototypeCell.screenshotsCollectionView.bounds.width
|
||||
self.prototypeCell.frame.size.width = widthConstraint.constant
|
||||
self.prototypeCell.layoutIfNeeded()
|
||||
|
||||
let collectionViewWidth = self.prototypeCell.screenshotsCollectionView.bounds.width
|
||||
let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down)
|
||||
let screenshotHeight = screenshotWidth * aspectRatio
|
||||
|
||||
let heightConstraint = prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
|
||||
let heightConstraint = self.prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
|
||||
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
|
||||
heightConstraint.isActive = true
|
||||
defer { heightConstraint.isActive = false }
|
||||
|
||||
let itemSize = prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
cachedItemSizes[item.bundleIdentifier] = itemSize
|
||||
let itemSize = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
self.cachedItemSizes[item.bundleIdentifier] = itemSize
|
||||
return itemSize
|
||||
}
|
||||
|
||||
override func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
let app = dataSource.item(at: indexPath)
|
||||
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||
{
|
||||
let app = self.dataSource.item(at: indexPath)
|
||||
|
||||
let appViewController = AppViewController.makeAppViewController(app: app)
|
||||
navigationController?.pushViewController(appViewController, animated: true)
|
||||
self.navigationController?.pushViewController(appViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowseViewController: UIViewControllerPreviewingDelegate {
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
|
||||
extension BrowseViewController: UIViewControllerPreviewingDelegate
|
||||
{
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
|
||||
{
|
||||
guard
|
||||
let indexPath = collectionView.indexPathForItem(at: location),
|
||||
let cell = collectionView.cellForItem(at: indexPath)
|
||||
let indexPath = self.collectionView.indexPathForItem(at: location),
|
||||
let cell = self.collectionView.cellForItem(at: indexPath)
|
||||
else { return nil }
|
||||
|
||||
|
||||
previewingContext.sourceRect = cell.frame
|
||||
|
||||
let app = dataSource.item(at: indexPath)
|
||||
|
||||
|
||||
let app = self.dataSource.item(at: indexPath)
|
||||
|
||||
let appViewController = AppViewController.makeAppViewController(app: app)
|
||||
return appViewController
|
||||
}
|
||||
|
||||
func previewingContext(_: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
|
||||
navigationController?.pushViewController(viewControllerToCommit, animated: true)
|
||||
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
|
||||
{
|
||||
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
|
||||
}
|
||||
}
|
||||
44
AltStore/Browse/ScreenshotCollectionViewCell.swift
Normal file
44
AltStore/Browse/ScreenshotCollectionViewCell.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// ScreenshotCollectionViewCell.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/15/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
@objc(ScreenshotCollectionViewCell)
|
||||
class ScreenshotCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
let imageView = UIImageView(image: nil)
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
private func initialize()
|
||||
{
|
||||
self.imageView.layer.masksToBounds = true
|
||||
self.addSubview(self.imageView, pinningEdgesWith: .zero)
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
self.imageView.layer.cornerRadius = 4
|
||||
}
|
||||
}
|
||||
144
AltStore/Components/AppBannerView.swift
Normal file
144
AltStore/Components/AppBannerView.swift
Normal file
@@ -0,0 +1,144 @@
|
||||
//
|
||||
// AppBannerView.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/29/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
class AppBannerView: RSTNibView
|
||||
{
|
||||
override var accessibilityLabel: String? {
|
||||
get { return self.accessibilityView?.accessibilityLabel }
|
||||
set { self.accessibilityView?.accessibilityLabel = newValue }
|
||||
}
|
||||
|
||||
override open var accessibilityAttributedLabel: NSAttributedString? {
|
||||
get { return self.accessibilityView?.accessibilityAttributedLabel }
|
||||
set { self.accessibilityView?.accessibilityAttributedLabel = newValue }
|
||||
}
|
||||
|
||||
override var accessibilityValue: String? {
|
||||
get { return self.accessibilityView?.accessibilityValue }
|
||||
set { self.accessibilityView?.accessibilityValue = newValue }
|
||||
}
|
||||
|
||||
override open var accessibilityAttributedValue: NSAttributedString? {
|
||||
get { return self.accessibilityView?.accessibilityAttributedValue }
|
||||
set { self.accessibilityView?.accessibilityAttributedValue = newValue }
|
||||
}
|
||||
|
||||
override open var accessibilityTraits: UIAccessibilityTraits {
|
||||
get { return self.accessibilityView?.accessibilityTraits ?? [] }
|
||||
set { self.accessibilityView?.accessibilityTraits = newValue }
|
||||
}
|
||||
|
||||
private var originalTintColor: UIColor?
|
||||
|
||||
@IBOutlet var titleLabel: UILabel!
|
||||
@IBOutlet var subtitleLabel: UILabel!
|
||||
@IBOutlet var iconImageView: AppIconImageView!
|
||||
@IBOutlet var button: PillButton!
|
||||
@IBOutlet var buttonLabel: UILabel!
|
||||
@IBOutlet var betaBadgeView: UIView!
|
||||
|
||||
@IBOutlet var backgroundEffectView: UIVisualEffectView!
|
||||
|
||||
@IBOutlet private var vibrancyView: UIVisualEffectView!
|
||||
@IBOutlet private var accessibilityView: UIView!
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder)
|
||||
{
|
||||
super.init(coder: coder)
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
private func initialize()
|
||||
{
|
||||
self.accessibilityView.accessibilityTraits.formUnion(.button)
|
||||
|
||||
self.isAccessibilityElement = false
|
||||
self.accessibilityElements = [self.accessibilityView, self.button].compactMap { $0 }
|
||||
|
||||
self.betaBadgeView.isHidden = true
|
||||
}
|
||||
|
||||
override func tintColorDidChange()
|
||||
{
|
||||
super.tintColorDidChange()
|
||||
|
||||
if self.tintAdjustmentMode != .dimmed
|
||||
{
|
||||
self.originalTintColor = self.tintColor
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppBannerView
|
||||
{
|
||||
func configure(for app: AppProtocol)
|
||||
{
|
||||
struct AppValues
|
||||
{
|
||||
var name: String
|
||||
var developerName: String? = nil
|
||||
var isBeta: Bool = false
|
||||
|
||||
init(app: AppProtocol)
|
||||
{
|
||||
self.name = app.name
|
||||
|
||||
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
|
||||
self.developerName = storeApp.developerName
|
||||
|
||||
if storeApp.isBeta
|
||||
{
|
||||
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
|
||||
self.isBeta = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let values = AppValues(app: app)
|
||||
self.titleLabel.text = app.name // Don't use values.name since that already includes "beta".
|
||||
self.betaBadgeView.isHidden = !values.isBeta
|
||||
|
||||
if let developerName = values.developerName
|
||||
{
|
||||
self.subtitleLabel.text = developerName
|
||||
self.accessibilityLabel = String(format: NSLocalizedString("%@ by %@", comment: ""), values.name, developerName)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.subtitleLabel.text = NSLocalizedString("Sideloaded", comment: "")
|
||||
self.accessibilityLabel = values.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppBannerView
|
||||
{
|
||||
func update()
|
||||
{
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 22
|
||||
|
||||
self.subtitleLabel.textColor = self.originalTintColor ?? self.tintColor
|
||||
self.backgroundEffectView.backgroundColor = self.originalTintColor ?? self.tintColor
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="SideStoreAppKit">
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/>
|
||||
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
|
||||
@@ -23,7 +23,7 @@
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="appBannerView" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5">
|
||||
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
@@ -46,7 +46,7 @@
|
||||
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="SideStore" customModuleProvider="target">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="14" y="14" width="60" height="60"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
|
||||
@@ -110,7 +110,7 @@
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
|
||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
43
AltStore/Components/AppIconImageView.swift
Normal file
43
AltStore/Components/AppIconImageView.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// AppIconImageView.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/9/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class AppIconImageView: UIImageView
|
||||
{
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.contentMode = .scaleAspectFill
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.backgroundColor = .white
|
||||
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
self.layer.cornerCurve = .continuous
|
||||
}
|
||||
else
|
||||
{
|
||||
if self.layer.responds(to: Selector(("continuousCorners")))
|
||||
{
|
||||
self.layer.setValue(true, forKey: "continuousCorners")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
// Based off of 60pt icon having 12pt radius.
|
||||
let radius = self.bounds.height / 5
|
||||
self.layer.cornerRadius = radius
|
||||
}
|
||||
}
|
||||
@@ -8,67 +8,78 @@
|
||||
|
||||
import AVFoundation
|
||||
|
||||
public final class BackgroundTaskManager {
|
||||
public static let shared = BackgroundTaskManager()
|
||||
|
||||
final class BackgroundTaskManager
|
||||
{
|
||||
static let shared = BackgroundTaskManager()
|
||||
|
||||
private var isPlaying = false
|
||||
|
||||
|
||||
private let audioEngine: AVAudioEngine
|
||||
private let player: AVAudioPlayerNode
|
||||
private let audioFile: AVAudioFile
|
||||
|
||||
|
||||
private let audioEngineQueue: DispatchQueue
|
||||
|
||||
private init() {
|
||||
audioEngine = AVAudioEngine()
|
||||
audioEngine.mainMixerNode.outputVolume = 0.0
|
||||
|
||||
player = AVAudioPlayerNode()
|
||||
audioEngine.attach(player)
|
||||
|
||||
do {
|
||||
|
||||
private init()
|
||||
{
|
||||
self.audioEngine = AVAudioEngine()
|
||||
self.audioEngine.mainMixerNode.outputVolume = 0.0
|
||||
|
||||
self.player = AVAudioPlayerNode()
|
||||
self.audioEngine.attach(self.player)
|
||||
|
||||
do
|
||||
{
|
||||
let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")!
|
||||
|
||||
audioFile = try AVAudioFile(forReading: audioFileURL)
|
||||
audioEngine.connect(player, to: audioEngine.mainMixerNode, format: audioFile.processingFormat)
|
||||
} catch {
|
||||
|
||||
self.audioFile = try AVAudioFile(forReading: audioFileURL)
|
||||
self.audioEngine.connect(self.player, to: self.audioEngine.mainMixerNode, format: self.audioFile.processingFormat)
|
||||
}
|
||||
catch
|
||||
{
|
||||
fatalError("Error. \(error)")
|
||||
}
|
||||
|
||||
audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager")
|
||||
|
||||
self.audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager")
|
||||
}
|
||||
}
|
||||
|
||||
public extension BackgroundTaskManager {
|
||||
func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void)) {
|
||||
func finish() {
|
||||
player.stop()
|
||||
audioEngine.stop()
|
||||
|
||||
isPlaying = false
|
||||
extension BackgroundTaskManager
|
||||
{
|
||||
func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void))
|
||||
{
|
||||
func finish()
|
||||
{
|
||||
self.player.stop()
|
||||
self.audioEngine.stop()
|
||||
|
||||
self.isPlaying = false
|
||||
}
|
||||
|
||||
audioEngineQueue.sync {
|
||||
do {
|
||||
|
||||
self.audioEngineQueue.sync {
|
||||
do
|
||||
{
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
|
||||
// Schedule audio file buffers.
|
||||
self.scheduleAudioFile()
|
||||
self.scheduleAudioFile()
|
||||
|
||||
|
||||
let outputFormat = self.audioEngine.outputNode.outputFormat(forBus: 0)
|
||||
self.audioEngine.connect(self.audioEngine.mainMixerNode, to: self.audioEngine.outputNode, format: outputFormat)
|
||||
|
||||
|
||||
try self.audioEngine.start()
|
||||
self.player.play()
|
||||
|
||||
|
||||
self.isPlaying = true
|
||||
|
||||
|
||||
taskHandler(.success(())) {
|
||||
finish()
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
taskHandler(.failure(error)) {
|
||||
finish()
|
||||
}
|
||||
@@ -77,9 +88,11 @@ public extension BackgroundTaskManager {
|
||||
}
|
||||
}
|
||||
|
||||
private extension BackgroundTaskManager {
|
||||
func scheduleAudioFile() {
|
||||
player.scheduleFile(audioFile, at: nil) {
|
||||
private extension BackgroundTaskManager
|
||||
{
|
||||
func scheduleAudioFile()
|
||||
{
|
||||
self.player.scheduleFile(self.audioFile, at: nil) {
|
||||
self.audioEngineQueue.async {
|
||||
guard self.isPlaying else { return }
|
||||
self.scheduleAudioFile()
|
||||
@@ -8,44 +8,46 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
final class BannerCollectionViewCell: UICollectionViewCell {
|
||||
final class BannerCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
private(set) var errorBadge: UIView?
|
||||
@IBOutlet private(set) var bannerView: AppBannerView!
|
||||
|
||||
override func awakeFromNib() {
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
|
||||
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
if #available(iOS 13.0, *)
|
||||
{
|
||||
let errorBadge = UIView()
|
||||
errorBadge.translatesAutoresizingMaskIntoConstraints = false
|
||||
errorBadge.isHidden = true
|
||||
self.addSubview(errorBadge)
|
||||
|
||||
|
||||
// Solid background to make the X opaque white.
|
||||
let backgroundView = UIView()
|
||||
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backgroundView.backgroundColor = .white
|
||||
errorBadge.addSubview(backgroundView)
|
||||
|
||||
|
||||
let badgeView = UIImageView(image: UIImage(systemName: "exclamationmark.circle.fill"))
|
||||
badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large)
|
||||
badgeView.tintColor = .systemRed
|
||||
errorBadge.addSubview(badgeView, pinningEdgesWith: .zero)
|
||||
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
errorBadge.centerXAnchor.constraint(equalTo: self.bannerView.trailingAnchor, constant: -5),
|
||||
errorBadge.centerYAnchor.constraint(equalTo: self.bannerView.topAnchor, constant: 5),
|
||||
|
||||
|
||||
backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor),
|
||||
backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor),
|
||||
backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5),
|
||||
backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5)
|
||||
])
|
||||
|
||||
|
||||
self.errorBadge = errorBadge
|
||||
}
|
||||
}
|
||||
@@ -8,50 +8,58 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
final class Button: UIButton {
|
||||
final class Button: UIButton
|
||||
{
|
||||
override var intrinsicContentSize: CGSize {
|
||||
var size = super.intrinsicContentSize
|
||||
size.width += 20
|
||||
size.height += 10
|
||||
return size
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
setTitleColor(.white, for: .normal)
|
||||
|
||||
layer.masksToBounds = true
|
||||
layer.cornerRadius = 8
|
||||
|
||||
update()
|
||||
|
||||
self.setTitleColor(.white, for: .normal)
|
||||
|
||||
self.layer.masksToBounds = true
|
||||
self.layer.cornerRadius = 8
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func tintColorDidChange() {
|
||||
|
||||
override func tintColorDidChange()
|
||||
{
|
||||
super.tintColorDidChange()
|
||||
|
||||
update()
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
|
||||
override var isHighlighted: Bool {
|
||||
didSet {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override var isEnabled: Bool {
|
||||
didSet {
|
||||
update()
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Button {
|
||||
func update() {
|
||||
if isEnabled {
|
||||
backgroundColor = tintColor
|
||||
} else {
|
||||
backgroundColor = .lightGray
|
||||
private extension Button
|
||||
{
|
||||
func update()
|
||||
{
|
||||
if self.isEnabled
|
||||
{
|
||||
self.backgroundColor = self.tintColor
|
||||
}
|
||||
else
|
||||
{
|
||||
self.backgroundColor = .lightGray
|
||||
}
|
||||
}
|
||||
}
|
||||
119
AltStore/Components/CollapsingTextView.swift
Normal file
119
AltStore/Components/CollapsingTextView.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// 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: CGFloat = 2 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
let moreButton = UIButton(type: .system)
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
extension CollapsingTextView: NSLayoutManagerDelegate
|
||||
{
|
||||
func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat
|
||||
{
|
||||
return self.lineSpacing
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,13 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
final class ForwardingNavigationController: UINavigationController {
|
||||
final class ForwardingNavigationController: UINavigationController
|
||||
{
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
self.topViewController
|
||||
return self.topViewController
|
||||
}
|
||||
|
||||
|
||||
override var childForStatusBarHidden: UIViewController? {
|
||||
topViewController
|
||||
return self.topViewController
|
||||
}
|
||||
}
|
||||
@@ -8,78 +8,93 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import RoxasUIKit
|
||||
import Roxas
|
||||
|
||||
@objc
|
||||
final class NavigationBar: UINavigationBar {
|
||||
@objc
|
||||
final class NavigationBar: UINavigationBar
|
||||
{
|
||||
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
|
||||
|
||||
|
||||
private let backgroundColorView = UIView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
initialize()
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
initialize()
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
private func initialize() {
|
||||
if #available(iOS 13, *) {
|
||||
|
||||
private func initialize()
|
||||
{
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
let standardAppearance = UINavigationBarAppearance()
|
||||
standardAppearance.configureWithDefaultBackground()
|
||||
standardAppearance.shadowColor = nil
|
||||
|
||||
|
||||
let edgeAppearance = UINavigationBarAppearance()
|
||||
edgeAppearance.configureWithOpaqueBackground()
|
||||
edgeAppearance.backgroundColor = self.barTintColor
|
||||
edgeAppearance.shadowColor = nil
|
||||
|
||||
if let tintColor = self.barTintColor {
|
||||
|
||||
if let tintColor = self.barTintColor
|
||||
{
|
||||
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
|
||||
|
||||
|
||||
standardAppearance.backgroundColor = tintColor
|
||||
standardAppearance.titleTextAttributes = textAttributes
|
||||
standardAppearance.largeTitleTextAttributes = textAttributes
|
||||
|
||||
|
||||
edgeAppearance.titleTextAttributes = textAttributes
|
||||
edgeAppearance.largeTitleTextAttributes = textAttributes
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
standardAppearance.backgroundColor = nil
|
||||
}
|
||||
|
||||
|
||||
self.scrollEdgeAppearance = edgeAppearance
|
||||
self.standardAppearance = standardAppearance
|
||||
} else {
|
||||
shadowImage = UIImage()
|
||||
|
||||
if let tintColor = barTintColor {
|
||||
backgroundColorView.backgroundColor = tintColor
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
self.shadowImage = UIImage()
|
||||
|
||||
if let tintColor = self.barTintColor
|
||||
{
|
||||
self.backgroundColorView.backgroundColor = tintColor
|
||||
|
||||
// Top = -50 to cover status bar area above navigation bar on any device.
|
||||
// Bottom = -1 to prevent a flickering gray line from appearing.
|
||||
addSubview(backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
|
||||
} else {
|
||||
barTintColor = .white
|
||||
self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.barTintColor = .white
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
if backgroundColorView.superview != nil {
|
||||
insertSubview(backgroundColorView, at: 1)
|
||||
|
||||
if self.backgroundColorView.superview != nil
|
||||
{
|
||||
self.insertSubview(self.backgroundColorView, at: 1)
|
||||
}
|
||||
|
||||
if automaticallyAdjustsItemPositions {
|
||||
|
||||
if self.automaticallyAdjustsItemPositions
|
||||
{
|
||||
// We can't easily shift just the back button up, so we shift the entire content view slightly.
|
||||
for contentView in subviews {
|
||||
for contentView in self.subviews
|
||||
{
|
||||
guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue }
|
||||
contentView.center.y -= 2
|
||||
}
|
||||
192
AltStore/Components/PillButton.swift
Normal file
192
AltStore/Components/PillButton.swift
Normal file
@@ -0,0 +1,192 @@
|
||||
//
|
||||
// PillButton.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/15/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
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 {
|
||||
var size = super.intrinsicContentSize
|
||||
size.width += 26
|
||||
size.height += 3
|
||||
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.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()
|
||||
}
|
||||
}
|
||||
|
||||
private extension PillButton
|
||||
{
|
||||
func update()
|
||||
{
|
||||
if self.progress == nil
|
||||
{
|
||||
self.setTitleColor(.white, for: .normal)
|
||||
self.backgroundColor = self.tintColor
|
||||
}
|
||||
else
|
||||
{
|
||||
self.setTitleColor(self.tintColor, for: .normal)
|
||||
self.backgroundColor = self.tintColor.withAlphaComponent(0.15)
|
||||
}
|
||||
|
||||
self.progressView.progressTintColor = self.tintColor
|
||||
}
|
||||
|
||||
@objc func updateCountdown()
|
||||
{
|
||||
guard let endDate = self.countdownDate else { return }
|
||||
|
||||
let startDate = Date()
|
||||
|
||||
let interval = endDate.timeIntervalSince(startDate)
|
||||
guard interval > 0 else {
|
||||
self.isEnabled = true
|
||||
return
|
||||
}
|
||||
|
||||
let text: String?
|
||||
|
||||
if interval < (1 * 60 * 60)
|
||||
{
|
||||
self.dateComponentsFormatter.unitsStyle = .positional
|
||||
self.dateComponentsFormatter.allowedUnits = [.minute, .second]
|
||||
|
||||
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
|
||||
}
|
||||
else if interval < (2 * 24 * 60 * 60)
|
||||
{
|
||||
self.dateComponentsFormatter.unitsStyle = .positional
|
||||
self.dateComponentsFormatter.allowedUnits = [.hour, .minute, .second]
|
||||
|
||||
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.dateComponentsFormatter.unitsStyle = .full
|
||||
self.dateComponentsFormatter.allowedUnits = [.day]
|
||||
|
||||
let numberOfDays = endDate.numberOfCalendarDays(since: startDate)
|
||||
text = String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays))
|
||||
}
|
||||
|
||||
if let text = text
|
||||
{
|
||||
UIView.performWithoutAnimation {
|
||||
self.isEnabled = false
|
||||
self.setTitle(text, for: .disabled)
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
public class TextCollectionReusableView: UICollectionReusableView {
|
||||
class TextCollectionReusableView: UICollectionReusableView
|
||||
{
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
|
||||
|
||||
@IBOutlet var topLayoutConstraint: NSLayoutConstraint!
|
||||
@IBOutlet var bottomLayoutConstraint: NSLayoutConstraint!
|
||||
@IBOutlet var leadingLayoutConstraint: NSLayoutConstraint!
|
||||
@@ -6,110 +6,125 @@
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import RoxasUIKit
|
||||
import Roxas
|
||||
|
||||
import SideStoreCore
|
||||
import SideKit
|
||||
import AltSign
|
||||
import AltStoreCore
|
||||
|
||||
extension TimeInterval {
|
||||
extension TimeInterval
|
||||
{
|
||||
static let shortToastViewDuration = 4.0
|
||||
static let longToastViewDuration = 8.0
|
||||
}
|
||||
|
||||
final class ToastView: RSTToastView {
|
||||
final class ToastView: RSTToastView
|
||||
{
|
||||
var preferredDuration: TimeInterval
|
||||
|
||||
override init(text: String, detailText detailedText: String?) {
|
||||
if detailedText == nil {
|
||||
preferredDuration = .shortToastViewDuration
|
||||
} else {
|
||||
preferredDuration = .longToastViewDuration
|
||||
|
||||
override init(text: String, detailText detailedText: String?)
|
||||
{
|
||||
if detailedText == nil
|
||||
{
|
||||
self.preferredDuration = .shortToastViewDuration
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
self.preferredDuration = .longToastViewDuration
|
||||
}
|
||||
|
||||
super.init(text: text, detailText: detailedText)
|
||||
|
||||
isAccessibilityElement = true
|
||||
|
||||
layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16)
|
||||
setNeedsLayout()
|
||||
|
||||
if let stackView = textLabel.superview as? UIStackView {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
convenience init(error: Error) {
|
||||
|
||||
convenience init(error: Error)
|
||||
{
|
||||
var error = error as NSError
|
||||
var underlyingError = error.underlyingError
|
||||
|
||||
|
||||
var preferredDuration: TimeInterval?
|
||||
|
||||
|
||||
if
|
||||
let unwrappedUnderlyingError = underlyingError,
|
||||
error.domain == AltServerErrorDomain && error.code == -1 //ALTServerError.underlyingError().rawValue
|
||||
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
|
||||
{
|
||||
// Treat underlyingError as the primary error.
|
||||
|
||||
|
||||
error = unwrappedUnderlyingError as NSError
|
||||
underlyingError = nil
|
||||
|
||||
|
||||
preferredDuration = .longToastViewDuration
|
||||
}
|
||||
|
||||
|
||||
let text: String
|
||||
let detailText: String?
|
||||
|
||||
if let failure = error.localizedFailure {
|
||||
|
||||
if let failure = error.localizedFailure
|
||||
{
|
||||
text = failure
|
||||
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
|
||||
} else if let reason = error.localizedFailureReason {
|
||||
}
|
||||
else if let reason = error.localizedFailureReason
|
||||
{
|
||||
text = reason
|
||||
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
text = error.localizedDescription
|
||||
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
|
||||
}
|
||||
|
||||
|
||||
self.init(text: text, detailText: detailText)
|
||||
|
||||
if let preferredDuration = preferredDuration {
|
||||
|
||||
if let preferredDuration = preferredDuration
|
||||
{
|
||||
self.preferredDuration = preferredDuration
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init(coder _: NSCoder) {
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
|
||||
// Rough calculation to determine height of ToastView with one-line textLabel.
|
||||
let minimumHeight = textLabel.font.lineHeight.rounded() + 18
|
||||
layer.cornerRadius = minimumHeight / 2
|
||||
let minimumHeight = self.textLabel.font.lineHeight.rounded() + 18
|
||||
self.layer.cornerRadius = minimumHeight/2
|
||||
}
|
||||
|
||||
func show(in viewController: UIViewController) {
|
||||
show(in: viewController.navigationController?.view ?? viewController.view, duration: preferredDuration)
|
||||
|
||||
func show(in viewController: UIViewController)
|
||||
{
|
||||
self.show(in: viewController.navigationController?.view ?? viewController.view, duration: self.preferredDuration)
|
||||
}
|
||||
|
||||
override func show(in view: UIView, duration: TimeInterval) {
|
||||
|
||||
override func show(in view: UIView, duration: TimeInterval)
|
||||
{
|
||||
super.show(in: view, duration: duration)
|
||||
|
||||
let announcement = (textLabel.text ?? "") + ". " + (detailTextLabel.text ?? "")
|
||||
accessibilityLabel = announcement
|
||||
|
||||
|
||||
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) {
|
||||
show(in: view, duration: preferredDuration)
|
||||
|
||||
override func show(in view: UIView)
|
||||
{
|
||||
self.show(in: view, duration: self.preferredDuration)
|
||||
}
|
||||
}
|
||||
17
AltStore/Consts/Consts+Proxy.swift
Normal file
17
AltStore/Consts/Consts+Proxy.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Proxy.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Joseph Mattiello on 11/7/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Consts {
|
||||
enum Proxy {
|
||||
static let address = "127.0.0.1"
|
||||
static let port = "51820"
|
||||
static let serverURL = "\(address):\(port)"
|
||||
}
|
||||
}
|
||||
@@ -8,4 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum Consts {}
|
||||
public enum Consts {
|
||||
|
||||
}
|
||||
@@ -7,28 +7,30 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
|
||||
extension FileManager {
|
||||
func directorySize(at directoryURL: URL) -> Int? {
|
||||
extension FileManager
|
||||
{
|
||||
func directorySize(at directoryURL: URL) -> Int?
|
||||
{
|
||||
guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
|
||||
|
||||
var total = 0
|
||||
|
||||
for case let fileURL as URL in enumerator {
|
||||
do {
|
||||
|
||||
var total: Int = 0
|
||||
|
||||
for case let fileURL as URL in enumerator
|
||||
{
|
||||
do
|
||||
{
|
||||
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
||||
guard let fileSize = resourceValues.fileSize else { continue }
|
||||
|
||||
|
||||
total += fileSize
|
||||
} catch {
|
||||
os_log("Failed to read file size for item: %@. %@", type: .error, fileURL.absoluteString, error.localizedDescription)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to read file size for item: \(fileURL).", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return total
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,13 @@ import Intents
|
||||
|
||||
// Requires iOS 14 in-app intent handling.
|
||||
@available(iOS 14, *)
|
||||
extension INInteraction {
|
||||
static func refreshAllApps() -> INInteraction {
|
||||
extension INInteraction
|
||||
{
|
||||
static func refreshAllApps() -> INInteraction
|
||||
{
|
||||
let refreshAllIntent = RefreshAllIntent()
|
||||
refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String
|
||||
|
||||
|
||||
let interaction = INInteraction(intent: refreshAllIntent, response: nil)
|
||||
return interaction
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import Foundation
|
||||
import OSLog
|
||||
|
||||
public let customLog = OSLog(subsystem: "org.sidestore.sidestore",
|
||||
category: "ios")
|
||||
category: "ios")
|
||||
|
||||
|
||||
public extension OSLog {
|
||||
/// Error logger extension
|
||||
@@ -21,7 +22,7 @@ public extension OSLog {
|
||||
static func error(_ message: StaticString, _ args: CVarArg...) {
|
||||
os_log(message, log: customLog, type: .error, args)
|
||||
}
|
||||
|
||||
|
||||
/// Info logger extension
|
||||
/// - Parameters:
|
||||
/// - message: String or format string
|
||||
@@ -30,7 +31,7 @@ public extension OSLog {
|
||||
static func info(_ message: StaticString, _ args: CVarArg...) {
|
||||
os_log(message, log: customLog, type: .info, args)
|
||||
}
|
||||
|
||||
|
||||
/// Debug logger extension
|
||||
/// - Parameters:
|
||||
/// - message: String or format string
|
||||
@@ -48,7 +49,7 @@ public extension OSLog {
|
||||
/// - message: String or format string
|
||||
/// - args: optional args for format string
|
||||
@inlinable
|
||||
public func ELOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) {
|
||||
public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||
OSLog.error(message, args)
|
||||
}
|
||||
|
||||
@@ -57,7 +58,7 @@ public func ELOG(_ message: StaticString, file _: StaticString = #file, function
|
||||
/// - message: String or format string
|
||||
/// - args: optional args for format string
|
||||
@inlinable
|
||||
public func ILOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) {
|
||||
public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||
OSLog.info(message, args)
|
||||
}
|
||||
|
||||
@@ -66,8 +67,8 @@ public func ILOG(_ message: StaticString, file _: StaticString = #file, function
|
||||
/// - message: String or format string
|
||||
/// - args: optional args for format string
|
||||
@inlinable
|
||||
public func DLOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) {
|
||||
public func DLOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||
OSLog.debug(message, args)
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
// mark: Helpers
|
||||
@@ -6,10 +6,11 @@
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import ARKit
|
||||
import UIKit
|
||||
import ARKit
|
||||
|
||||
extension UIDevice {
|
||||
extension UIDevice
|
||||
{
|
||||
var isJailbroken: Bool {
|
||||
if
|
||||
FileManager.default.fileExists(atPath: "/Applications/Cydia.app") ||
|
||||
@@ -18,29 +19,31 @@ extension UIDevice {
|
||||
FileManager.default.fileExists(atPath: "/usr/sbin/sshd") ||
|
||||
FileManager.default.fileExists(atPath: "/etc/apt") ||
|
||||
FileManager.default.fileExists(atPath: "/private/var/lib/apt/") ||
|
||||
UIApplication.shared.canOpenURL(URL(string: "cydia://")!)
|
||||
UIApplication.shared.canOpenURL(URL(string:"cydia://")!)
|
||||
{
|
||||
return true
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 14, *)
|
||||
var supportsFugu14: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return true
|
||||
return true
|
||||
#else
|
||||
// Fugu14 is supported on devices with an A12 processor or better.
|
||||
// ARKit 3 is only supported by devices with an A12 processor or better, according to the documentation.
|
||||
return ARBodyTrackingConfiguration.isSupported
|
||||
// Fugu14 is supported on devices with an A12 processor or better.
|
||||
// ARKit 3 is only supported by devices with an A12 processor or better, according to the documentation.
|
||||
return ARBodyTrackingConfiguration.isSupported
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 14, *)
|
||||
var isUntetheredJailbreakRequired: Bool {
|
||||
let ios14_4 = OperatingSystemVersion(majorVersion: 14, minorVersion: 4, patchVersion: 0)
|
||||
|
||||
|
||||
let isUntetheredJailbreakRequired = ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14_4)
|
||||
return isUntetheredJailbreakRequired
|
||||
}
|
||||
@@ -8,32 +8,37 @@
|
||||
|
||||
import AudioToolbox
|
||||
import CoreHaptics
|
||||
import UIKit
|
||||
|
||||
private extension SystemSoundID {
|
||||
private extension SystemSoundID
|
||||
{
|
||||
static let pop = SystemSoundID(1520)
|
||||
static let cancelled = SystemSoundID(1521)
|
||||
static let tryAgain = SystemSoundID(1102)
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
extension UIDevice {
|
||||
enum VibrationPattern {
|
||||
extension UIDevice
|
||||
{
|
||||
enum VibrationPattern
|
||||
{
|
||||
case success
|
||||
case error
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
extension UIDevice {
|
||||
extension UIDevice
|
||||
{
|
||||
var isVibrationSupported: Bool {
|
||||
CHHapticEngine.capabilitiesForHardware().supportsHaptics
|
||||
return CHHapticEngine.capabilitiesForHardware().supportsHaptics
|
||||
}
|
||||
|
||||
func vibrate(pattern: VibrationPattern) {
|
||||
guard isVibrationSupported else { return }
|
||||
|
||||
switch pattern {
|
||||
func vibrate(pattern: VibrationPattern)
|
||||
{
|
||||
guard self.isVibrationSupported else { return }
|
||||
|
||||
switch pattern
|
||||
{
|
||||
case .success: AudioServicesPlaySystemSound(.tryAgain)
|
||||
case .error: AudioServicesPlaySystemSound(.cancelled)
|
||||
}
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIScreen {
|
||||
extension UIScreen
|
||||
{
|
||||
var isExtraCompactHeight: Bool {
|
||||
fixedCoordinateSpace.bounds.height < 600
|
||||
return self.fixedCoordinateSpace.bounds.height < 600
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ALTAnisetteURL</key>
|
||||
<string>https://ani.sidestore.io</string>
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
@@ -11,10 +9,12 @@
|
||||
</array>
|
||||
<key>ALTDeviceID</key>
|
||||
<string>00008101-000129D63698001E</string>
|
||||
<key>ALTPairingFile</key>
|
||||
<string><insert pairing file here></string>
|
||||
<key>ALTServerID</key>
|
||||
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
||||
<key>ALTPairingFile</key>
|
||||
<string><insert pairing file here></string>
|
||||
<key>ALTAnisetteURL</key>
|
||||
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
@@ -44,6 +44,8 @@
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -91,13 +93,6 @@
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_altserver._tcp</string>
|
||||
@@ -136,10 +131,13 @@
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
@@ -206,5 +204,7 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -7,74 +7,84 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SideStoreCore
|
||||
import Intents
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
@available(iOS 14, *)
|
||||
public final class IntentHandler: NSObject, RefreshAllIntentHandling {
|
||||
final class IntentHandler: NSObject, RefreshAllIntentHandling
|
||||
{
|
||||
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
|
||||
|
||||
|
||||
private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]()
|
||||
private var queuedResponses = [RefreshAllIntent: RefreshAllIntentResponse]()
|
||||
|
||||
|
||||
private var operations = [RefreshAllIntent: BackgroundRefreshAppsOperation]()
|
||||
|
||||
public func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) {
|
||||
|
||||
func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
|
||||
{
|
||||
// Refreshing apps usually, but not always, completes within alotted time.
|
||||
// As a workaround, we'll start refreshing apps in confirm() so we can
|
||||
// take advantage of some extra time before starting handle() timeout timer.
|
||||
|
||||
completionHandlers[intent] = { response in
|
||||
if response.code != .ready {
|
||||
self.completionHandlers[intent] = { (response) in
|
||||
if response.code != .ready
|
||||
{
|
||||
// Operation finished before confirmation "timeout".
|
||||
// Cache response to return it when handle() is called.
|
||||
self.queuedResponses[intent] = response
|
||||
}
|
||||
|
||||
|
||||
completion(RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
||||
}
|
||||
|
||||
|
||||
// Give ourselves 9 extra seconds before starting handle() timeout timer.
|
||||
// 10 seconds or longer results in timeout regardless.
|
||||
queue.asyncAfter(deadline: .now() + 9.0) {
|
||||
self.queue.asyncAfter(deadline: .now() + 9.0) {
|
||||
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
||||
}
|
||||
|
||||
if !DatabaseManager.shared.isStarted {
|
||||
DatabaseManager.shared.start { error in
|
||||
if let error = error {
|
||||
|
||||
if !DatabaseManager.shared.isStarted
|
||||
{
|
||||
DatabaseManager.shared.start() { (error) in
|
||||
if let error = error
|
||||
{
|
||||
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription))
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
self.refreshApps(intent: intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
refreshApps(intent: intent)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.refreshApps(intent: intent)
|
||||
}
|
||||
}
|
||||
|
||||
public func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) {
|
||||
completionHandlers[intent] = { response in
|
||||
|
||||
func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
|
||||
{
|
||||
self.completionHandlers[intent] = { (response) in
|
||||
// Ignore .ready response from confirm() timeout.
|
||||
guard response.code != .ready else { return }
|
||||
completion(response)
|
||||
}
|
||||
|
||||
if let response = queuedResponses[intent] {
|
||||
queuedResponses[intent] = nil
|
||||
finish(intent, response: response)
|
||||
} else {
|
||||
queue.asyncAfter(deadline: .now() + 7.0) {
|
||||
if let operation = self.operations[intent] {
|
||||
if let response = self.queuedResponses[intent]
|
||||
{
|
||||
self.queuedResponses[intent] = nil
|
||||
self.finish(intent, response: response)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.queue.asyncAfter(deadline: .now() + 7.0) {
|
||||
if let operation = self.operations[intent]
|
||||
{
|
||||
// We took too long to finish and return the final result,
|
||||
// so we'll now present a normal notification when finished.
|
||||
operation.presentsFinishedNotification = true
|
||||
}
|
||||
|
||||
|
||||
self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil))
|
||||
}
|
||||
}
|
||||
@@ -82,42 +92,54 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
private extension IntentHandler {
|
||||
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse) {
|
||||
queue.async {
|
||||
if let completionHandler = self.completionHandlers[intent] {
|
||||
private extension IntentHandler
|
||||
{
|
||||
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse)
|
||||
{
|
||||
self.queue.async {
|
||||
if let completionHandler = self.completionHandlers[intent]
|
||||
{
|
||||
self.completionHandlers[intent] = nil
|
||||
completionHandler(response)
|
||||
} else if response.code != .ready && response.code != .inProgress {
|
||||
}
|
||||
else if response.code != .ready && response.code != .inProgress
|
||||
{
|
||||
// Queue response in case refreshing finishes after confirm() but before handle().
|
||||
self.queuedResponses[intent] = response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshApps(intent: RefreshAllIntent) {
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||
|
||||
func refreshApps(intent: RefreshAllIntent)
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApps = InstalledApp.fetchActiveApps(in: context)
|
||||
let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { result in
|
||||
do {
|
||||
let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { (result) in
|
||||
do
|
||||
{
|
||||
let results = try result.get()
|
||||
|
||||
for (_, result) in results {
|
||||
|
||||
for (_, result) in results
|
||||
{
|
||||
guard case let .failure(error) = result else { continue }
|
||||
throw error
|
||||
}
|
||||
|
||||
|
||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||
} catch RefreshError.noInstalledApps {
|
||||
}
|
||||
catch RefreshError.noInstalledApps
|
||||
{
|
||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||
} catch let error as NSError {
|
||||
os_log("Failed to refresh apps in background. %@", type: .error , error.localizedDescription)
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
print("Failed to refresh apps in background.", error)
|
||||
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription))
|
||||
}
|
||||
|
||||
|
||||
self.operations[intent] = nil
|
||||
}
|
||||
|
||||
|
||||
self.operations[intent] = operation
|
||||
}
|
||||
}
|
||||
@@ -6,58 +6,58 @@
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Roxas
|
||||
import EmotionalDamage
|
||||
import minimuxer
|
||||
import MiniMuxer
|
||||
import SideStoreAppKit
|
||||
import RoxasUIKit
|
||||
import UIKit
|
||||
|
||||
import SideStoreCore
|
||||
import AltStoreCore
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate {
|
||||
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
|
||||
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
|
||||
DatabaseManager.shared.start(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
return [isDatabaseStarted]
|
||||
}
|
||||
|
||||
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
self.children.first
|
||||
return self.children.first
|
||||
}
|
||||
|
||||
|
||||
override var childForStatusBarHidden: UIViewController? {
|
||||
self.children.first
|
||||
return self.children.first
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
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(_: Bool) {
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(true)
|
||||
#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)
|
||||
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 fetchPairingFile() -> String? {
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fm = FileManager.default
|
||||
@@ -70,20 +70,19 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
fm.fileExists(atPath: appResourcePath.path),
|
||||
let data = fm.contents(atPath: appResourcePath.path),
|
||||
let contents = String(data: data, encoding: .utf8),
|
||||
!contents.isEmpty
|
||||
{
|
||||
!contents.isEmpty {
|
||||
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") {
|
||||
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
|
||||
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 for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert)
|
||||
|
||||
|
||||
// Create OK button with action handler
|
||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { _ in
|
||||
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))
|
||||
@@ -92,13 +91,13 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
documentPickerController.shouldShowFileExtensions = true
|
||||
documentPickerController.delegate = self
|
||||
self.present(documentPickerController, animated: true, completion: nil)
|
||||
})
|
||||
|
||||
// Add OK button to a dialog message
|
||||
})
|
||||
|
||||
//Add OK button to a dialog message
|
||||
dialogMessage.addAction(ok)
|
||||
|
||||
// Present Alert to
|
||||
present(dialogMessage, animated: true, completion: nil)
|
||||
self.present(dialogMessage, animated: true, completion: nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -110,9 +109,9 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
|
||||
|
||||
// Present alert to user
|
||||
present(dialogMessage, animated: true, completion: nil)
|
||||
self.present(dialogMessage, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
let url = urls[0]
|
||||
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
|
||||
@@ -124,42 +123,42 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
if pairing_string == nil {
|
||||
displayError("Unable to read pairing file")
|
||||
}
|
||||
|
||||
|
||||
// Save to a file for next launch
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fm = FileManager.default
|
||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
|
||||
|
||||
|
||||
// Start minimuxer now that we have a file
|
||||
start_minimuxer_threads(pairing_string!)
|
||||
|
||||
|
||||
} catch {
|
||||
displayError("Unable to read pairing file")
|
||||
}
|
||||
|
||||
if isSecuredURL {
|
||||
|
||||
if (isSecuredURL) {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
controller.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func documentPickerWasCancelled(_: UIDocumentPickerViewController) {
|
||||
|
||||
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) {
|
||||
set_usbmuxd_socket()
|
||||
#if false // Retries
|
||||
var res = start_minimuxer(pairing_file: pairing_file)
|
||||
var attempts = 10
|
||||
while attempts != 0, res != 0 {
|
||||
print("start_minimuxer `res` != 0, retry #\(attempts)")
|
||||
res = start_minimuxer(pairing_file: pairing_file)
|
||||
attempts -= 1
|
||||
}
|
||||
var res = start_minimuxer(pairing_file: pairing_file)
|
||||
var attempts = 10
|
||||
while (attempts != 0 && res != 0) {
|
||||
print("start_minimuxer `res` != 0, retry #\(attempts)")
|
||||
res = start_minimuxer(pairing_file: pairing_file)
|
||||
attempts -= 1
|
||||
}
|
||||
#else
|
||||
let res = start_minimuxer(pairing_file: pairing_file)
|
||||
let res = start_minimuxer(pairing_file: pairing_file)
|
||||
#endif
|
||||
if res != 0 {
|
||||
displayError("minimuxer failed to start. Incorrect arguments were passed.")
|
||||
@@ -168,51 +167,60 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
}
|
||||
}
|
||||
|
||||
extension LaunchViewController {
|
||||
override func handleLaunchError(_ error: Error) {
|
||||
do {
|
||||
extension LaunchViewController
|
||||
{
|
||||
override func handleLaunchError(_ error: Error)
|
||||
{
|
||||
do
|
||||
{
|
||||
throw error
|
||||
} catch let error as NSError {
|
||||
}
|
||||
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, *) {
|
||||
|
||||
if #available(iOS 14.5, *)
|
||||
{
|
||||
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
|
||||
errorDescription = errorMessages.joined(separator: "\n\n")
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
errorDescription = error.debugDescription
|
||||
}
|
||||
|
||||
|
||||
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { _ in
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
||||
self.handleLaunchConditions()
|
||||
}))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func finishLaunching() {
|
||||
|
||||
override func finishLaunching()
|
||||
{
|
||||
super.finishLaunching()
|
||||
|
||||
guard !didFinishLaunching else { return }
|
||||
|
||||
|
||||
guard !self.didFinishLaunching else { return }
|
||||
|
||||
AppManager.shared.update()
|
||||
AppManager.shared.updatePatronsIfNeeded()
|
||||
AppManager.shared.updatePatronsIfNeeded()
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
|
||||
|
||||
// Add view controller as child (rather than presenting modally)
|
||||
// so tint adjustment + card presentations works correctly.
|
||||
destinationViewController.view.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
|
||||
destinationViewController.view.alpha = 0.0
|
||||
addChild(destinationViewController)
|
||||
view.addSubview(destinationViewController.view, pinningEdgesWith: .zero)
|
||||
destinationViewController.didMove(toParent: self)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
didFinishLaunching = true
|
||||
|
||||
self.didFinishLaunching = true
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
86
AltStore/Managing Apps/AppManagerErrors.swift
Normal file
86
AltStore/Managing Apps/AppManagerErrors.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// 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 errorDescription: String? {
|
||||
if let error = self.primaryError
|
||||
{
|
||||
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] {
|
||||
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
|
||||
return [NSUnderlyingErrorKey: error]
|
||||
}
|
||||
|
||||
init(_ error: Error)
|
||||
{
|
||||
self.primaryError = error
|
||||
}
|
||||
|
||||
init(sources: Set<Source>, errors: [Source: Error], context: NSManagedObjectContext)
|
||||
{
|
||||
self.sources = sources
|
||||
self.errors = errors
|
||||
self.managedObjectContext = context
|
||||
}
|
||||
}
|
||||
}
|
||||
44
AltStore/My Apps/InstalledAppsCollectionHeaderView.swift
Normal file
44
AltStore/My Apps/InstalledAppsCollectionHeaderView.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// 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,38 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="installedAppsCollectionHeaderView" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsHeader" id="eyV-eW-aLi" customClass="InstalledAppsCollectionHeaderView" customModule="SideStoreAppKit">
|
||||
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsHeader" id="eyV-eW-aLi" customClass="InstalledAppsCollectionHeaderView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Installed" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zhW-Re-WNf">
|
||||
<rect key="frame" x="20" y="21" width="96" height="29"/>
|
||||
<rect key="frame" x="20" y="21" width="96.5" height="29"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iMf-wr-wRV">
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iMf-wr-wRV">
|
||||
<rect key="frame" x="274" y="23" width="81" height="32"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/>
|
||||
<state key="normal" title="Refresh All"/>
|
||||
</button>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="N3q-SZ-Vyv"/>
|
||||
<constraints>
|
||||
<constraint firstItem="zhW-Re-WNf" firstAttribute="leading" secondItem="eyV-eW-aLi" secondAttribute="leading" constant="20" id="Fo0-fL-UpD"/>
|
||||
<constraint firstAttribute="bottom" secondItem="zhW-Re-WNf" secondAttribute="bottom" id="OWw-FY-KOh"/>
|
||||
<constraint firstAttribute="trailing" secondItem="iMf-wr-wRV" secondAttribute="trailing" constant="20" id="dJM-7c-k31"/>
|
||||
<constraint firstItem="iMf-wr-wRV" firstAttribute="firstBaseline" secondItem="zhW-Re-WNf" secondAttribute="firstBaseline" id="iU7-F2-XDu"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="N3q-SZ-Vyv"/>
|
||||
<connections>
|
||||
<outlet property="button" destination="iMf-wr-wRV" id="kWT-cc-BjS"/>
|
||||
<outlet property="textLabel" destination="zhW-Re-WNf" id="UOg-4X-rWx"/>
|
||||
@@ -6,89 +6,92 @@
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import RoxasUIKit
|
||||
import UIKit
|
||||
import Roxas
|
||||
|
||||
@objc
|
||||
final class InstalledAppCollectionViewCell: UICollectionViewCell {
|
||||
final class InstalledAppCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
private(set) var deactivateBadge: UIView?
|
||||
|
||||
|
||||
@IBOutlet var bannerView: AppBannerView!
|
||||
|
||||
override func awakeFromNib() {
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
|
||||
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
if #available(iOS 13.0, *)
|
||||
{
|
||||
let deactivateBadge = UIView()
|
||||
deactivateBadge.translatesAutoresizingMaskIntoConstraints = false
|
||||
deactivateBadge.isHidden = true
|
||||
self.addSubview(deactivateBadge)
|
||||
|
||||
|
||||
// Solid background to make the X opaque white.
|
||||
let backgroundView = UIView()
|
||||
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backgroundView.backgroundColor = .white
|
||||
deactivateBadge.addSubview(backgroundView)
|
||||
|
||||
|
||||
let badgeView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
|
||||
badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large)
|
||||
badgeView.tintColor = .systemRed
|
||||
deactivateBadge.addSubview(badgeView, pinningEdgesWith: .zero)
|
||||
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
deactivateBadge.centerXAnchor.constraint(equalTo: self.bannerView.iconImageView.trailingAnchor),
|
||||
deactivateBadge.centerYAnchor.constraint(equalTo: self.bannerView.iconImageView.topAnchor),
|
||||
|
||||
|
||||
backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor),
|
||||
backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor),
|
||||
backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5),
|
||||
backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5)
|
||||
])
|
||||
|
||||
|
||||
self.deactivateBadge = deactivateBadge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
final class InstalledAppsCollectionFooterView: UICollectionReusableView {
|
||||
final class InstalledAppsCollectionFooterView: UICollectionReusableView
|
||||
{
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
@IBOutlet var button: UIButton!
|
||||
}
|
||||
|
||||
@objc
|
||||
final class NoUpdatesCollectionViewCell: UICollectionViewCell {
|
||||
final class NoUpdatesCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
@IBOutlet var blurView: UIVisualEffectView!
|
||||
|
||||
override func awakeFromNib() {
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
final class UpdatesCollectionHeaderView: UICollectionReusableView {
|
||||
final class UpdatesCollectionHeaderView: UICollectionReusableView
|
||||
{
|
||||
let button = PillButton(type: .system)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.setTitle(">", for: .normal)
|
||||
addSubview(button)
|
||||
|
||||
NSLayoutConstraint.activate([button.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
|
||||
button.topAnchor.constraint(equalTo: topAnchor),
|
||||
button.widthAnchor.constraint(equalToConstant: 50),
|
||||
button.heightAnchor.constraint(equalToConstant: 26)])
|
||||
|
||||
self.button.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.button.setTitle(">", for: .normal)
|
||||
self.addSubview(self.button)
|
||||
|
||||
NSLayoutConstraint.activate([self.button.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
|
||||
self.button.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
self.button.widthAnchor.constraint(equalToConstant: 50),
|
||||
self.button.heightAnchor.constraint(equalToConstant: 26)])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user