Compare commits

..

7 Commits

Author SHA1 Message Date
Joseph Mattello
e9d3060df7 Swift PM switch roxas fork
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-02-26 21:27:42 -05:00
Joseph Mattello
c59043068e Package.swift Roxas use SideStore port
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-02-26 21:27:42 -05:00
Joseph Mattello
65c43d683c Package.swift builds but errors
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-02-26 21:27:42 -05:00
Joseph Mattello
9e6147c860 Add Package.swift for Danger
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-02-26 21:27:42 -05:00
Joseph Mattello
b3074cadf9 Add Danger github action
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-02-26 21:27:42 -05:00
Joseph Mattello
9c5c597ce6 Add Dangerfile copied from Provenance 2023-02-26 21:27:42 -05:00
Joseph Mattello
977a452605 Add Dangerfile.swift
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-02-26 21:27:40 -05:00
481 changed files with 16622 additions and 19344 deletions

View File

@@ -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

View File

@@ -1,35 +1,39 @@
# http://editorconfig.org # EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true root = true
# Unix-style newlines with a newline ending every file
[*] [*]
indent_style = space
charset = utf-8
indent_size = 4
end_of_line = lf end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.{md,markdown}] # Matches multiple files with brace expansion notation
trim_trailing_whitespace = false # Set default charset
[*.{js,py}]
charset = utf-8# 4 space indentation
[*.{c,h,m,mm}] # Swift files
trim_trailing_whitespace = true [*.swift]
indent_style = space
indent_size = 4
charset = utf-8# 4 space indentation
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.js] # Matches the exact files either package.json or .travis.yml
indent_size = 2 [{package.json,.travis.yml}]
indent_style = space
[*.{swift}]
trim_trailing_whitespace = true
indent_style = tab
indent_size = 4
[Makefile]
trim_trailing_whitespace = true
indent_style = tab
indent_size = 8
[*.{yaml|yml}]
indent_size = 2 indent_size = 2

0
.env
View File

View File

@@ -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: ''

View File

@@ -2,7 +2,7 @@ name: Beta SideStore build
on: on:
push: push:
tags: 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: jobs:
build: build:
@@ -11,80 +11,74 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-12' - os: 'macos-12'
version: '14.2' version: '14.2'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: recursive
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Change version to tag - name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: make build | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app - name: Fakesign app
run: make fakesign run: make fakesign
- name: Convert to IPA - name: Convert to IPA
run: make 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 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-dSYM name: SideStore.ipa
path: ./*.dSYM/ path: SideStore.ipa
- name: Get version - name: Get version
id: version id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date - name: Get current date
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in SideStore date form - name: Get current date in AltStore date form
id: date_sidestore id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to new beta release - name: Upload to new beta release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }} name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
draft: true draft: true
prerelease: true prerelease: true
files: SideStore.ipa files: SideStore.ipa
body: | 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. --> <!-- 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!** 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 ## Changelog
- TODO - TODO
## Build Info ## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}` Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`

13
.github/workflows/danger.yml vendored Normal file
View 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 }}

View File

@@ -2,7 +2,7 @@ name: Nightly SideStore build
on: on:
push: push:
branches: branches:
- develop - develop
jobs: jobs:
build: build:
@@ -14,87 +14,81 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-12' - os: 'macos-12'
version: '14.2' version: '14.2'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: recursive
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Cache .nightly-build-num - name: Cache .nightly-build-num
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: .nightly-build-num path: .nightly-build-num
key: nightly-build-num key: nightly-build-num
- name: Increase nightly build number and set as version - name: Increase nightly build number and set as version
run: bash .github/workflows/increase-nightly-build-num.sh run: bash .github/workflows/increase-nightly-build-num.sh
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: make build | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app - name: Fakesign app
run: make fakesign run: make fakesign
- name: Convert to IPA - name: Convert to IPA
run: make 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 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-dSYM name: SideStore.ipa
path: ./*.dSYM/ path: SideStore.ipa
- name: Get version - name: Get version
id: version id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date - name: Get current date
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in SideStore date form - name: Get current date in AltStore date form
id: date_sidestore id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to nightly release - name: Upload to nightly release
uses: IsaacShelton/update-existing-release@v1.3.1 uses: IsaacShelton/update-existing-release@v1.3.1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
release: "Nightly" release: "Nightly"
tag: "nightly" tag: "nightly"
prerelease: true prerelease: true
files: SideStore.ipa files: SideStore.ipa
body: | body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}). 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!** 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). 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 ## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}` Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`
- name: Reset cache for apps.sidestore.io/nightly - name: Reset cache for apps.sidestore.io/nightly
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }} run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}

View File

@@ -39,14 +39,8 @@ jobs:
- name: Convert to IPA - name: Convert to IPA
run: make ipa run: make ipa
- name: Upload SideStore.ipa Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore.ipa name: SideStore.ipa
path: SideStore.ipa path: SideStore.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-dSYM
path: ./*.dSYM/

View File

@@ -2,7 +2,7 @@ name: Stable SideStore build
on: on:
push: push:
tags: tags:
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0 - '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
jobs: jobs:
build: build:
@@ -11,77 +11,71 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-12' - os: 'macos-12'
version: '14.2' version: '14.2'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: recursive
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Change version to tag - name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: make build | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app - name: Fakesign app
run: make fakesign run: make fakesign
- name: Convert to IPA - name: Convert to IPA
run: make 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 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-dSYM name: SideStore.ipa
path: ./*.dSYM/ path: SideStore.ipa
- name: Get version - name: Get version
id: version id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date - name: Get current date
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in SideStore date form - name: Get current date in AltStore date form
id: date_sidestore id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to new stable release - name: Upload to new stable release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }} name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
draft: true draft: true
files: SideStore.ipa files: SideStore.ipa
body: | 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. --> <!-- 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 ## Changelog
- TODO - TODO
## Build Info ## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}` Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`

4
.gitignore vendored
View File

@@ -35,10 +35,10 @@ xcuserdata
## AppCode specific ## AppCode specific
.idea/ .idea/
.build
Payload/ Payload/
SideStore.ipa SideStore.ipa
*.dSYM
Dependencies/.*-prebuilt-fetch-* Dependencies/.*-prebuilt-fetch-*
Dependencies/minimuxer/* Dependencies/minimuxer/*
Dependencies/em_proxy/* Dependencies/em_proxy/*

27
.gitmodules vendored
View File

@@ -1,6 +1,21 @@
[submodule "Dependencies/em_proxy"] [submodule "Dependencies/Roxas"]
path = SideStoreApp/Dependencies/em_proxy path = Dependencies/Roxas
url = https://github.com/SideStore/em_proxy.git url = https://github.com/rileytestut/Roxas.git
[submodule "Dependencies/minimuxer"] [submodule "Dependencies/libimobiledevice"]
path = SideStoreApp/Dependencies/minimuxer path = Dependencies/libimobiledevice
url = https://github.com/SideStore/minimuxer.git url = https://github.com/libimobiledevice/libimobiledevice
[submodule "Dependencies/libusbmuxd"]
path = Dependencies/libusbmuxd
url = https://github.com/libimobiledevice/libusbmuxd.git
[submodule "Dependencies/libplist"]
path = Dependencies/libplist
url = https://github.com/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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1,3 @@
#include "Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup

View File

@@ -7,12 +7,9 @@
// //
import UIKit import UIKit
import OSLog
#if canImport(Logging)
import Logging
#endif
extension AppDelegate { extension AppDelegate
{
static let startBackupNotification = Notification.Name("io.altstore.StartBackup") static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore") static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
@@ -23,58 +20,64 @@ extension AppDelegate {
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
private var currentBackupReturnURL: URL? private var currentBackupReturnURL: URL?
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Override point for customization after application launch. // Override point for customization after application launch.
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
let viewController = ViewController() let viewController = ViewController()
window = UIWindow(frame: UIScreen.main.bounds) self.window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = viewController self.window?.rootViewController = viewController
window?.makeKeyAndVisible() self.window?.makeKeyAndVisible()
return true return true
} }
func applicationWillResignActive(_: UIApplication) { func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
} }
func applicationDidBecomeActive(_: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
} }
func applicationWillTerminate(_: UIApplication) { func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
} }
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
open(url) {
return self.open(url)
} }
} }
private extension AppDelegate { private extension AppDelegate
func open(_ url: URL) -> Bool { {
func open(_ url: URL) -> Bool
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let command = components.host?.lowercased() else { return false } guard let command = components.host?.lowercased() else { return false }
switch command { switch command
{
case "backup": case "backup":
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false } guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
currentBackupReturnURL = returnURL self.currentBackupReturnURL = returnURL
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
return true return true
case "restore": case "restore":
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false } guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
currentBackupReturnURL = returnURL self.currentBackupReturnURL = returnURL
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
return true return true
@@ -83,21 +86,23 @@ private extension AppDelegate {
} }
} }
@objc func operationDidFinish(_ notification: Notification) { @objc func operationDidFinish(_ notification: Notification)
{
defer { self.currentBackupReturnURL = nil } defer { self.currentBackupReturnURL = nil }
guard guard
let returnURL = currentBackupReturnURL, let returnURL = self.currentBackupReturnURL,
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error> let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
else { return } else { return }
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return } guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
switch result { switch result
{
case .success: case .success:
components.path = "/success" components.path = "/success"
case let .failure(error as NSError): case .failure(let error as NSError):
components.path = "/failure" components.path = "/failure"
components.queryItems = ["errorDomain": error.domain, components.queryItems = ["errorDomain": error.domain,
"errorCode": String(error.code), "errorCode": String(error.code),
@@ -107,9 +112,10 @@ private extension AppDelegate {
guard let responseURL = components.url else { return } guard let responseURL = components.url else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
UIApplication.shared.open(responseURL, options: [:]) { success in UIApplication.shared.open(responseURL, options: [:]) { (success) in
os_log("Sent response to app with success: %@", type: .info , success) print("Sent response to app with success:", success)
} }
} }
} }
} }

View File

@@ -7,21 +7,15 @@
// //
import Foundation import Foundation
import OSLog
#if canImport(Logging)
import Logging
#endif
import AltSign extension ErrorUserInfoKey
import Roxas {
import protocol SideStoreCore.ALTLocalizedError
extension ErrorUserInfoKey {
static let sourceFile: String = "alt_sourceFile" static let sourceFile: String = "alt_sourceFile"
static let sourceFileLine: String = "alt_sourceFileLine" static let sourceFileLine: String = "alt_sourceFileLine"
} }
extension Error { extension Error
{
var sourceDescription: String? { var sourceDescription: String? {
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else { guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
return nil return nil
@@ -30,8 +24,10 @@ extension Error {
} }
} }
struct BackupError: ALTLocalizedError { struct BackupError: ALTLocalizedError
enum Code { {
enum Code
{
case invalidBundleID case invalidBundleID
case appGroupNotFound(String?) case appGroupNotFound(String?)
case randomError // Used for debugging. case randomError // Used for debugging.
@@ -45,45 +41,54 @@ struct BackupError: ALTLocalizedError {
var failure: String? var failure: String?
var failureReason: String? { var failureReason: String? {
switch code { switch self.code
{
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "") case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
case let .appGroupNotFound(appGroup): case .appGroupNotFound(let appGroup):
if let appGroup = appGroup { if let appGroup = appGroup
{
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup) return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
} else { }
else
{
return NSLocalizedString("The AltStore app group could not be found.", comment: "") return NSLocalizedString("The AltStore app group could not be found.", comment: "")
} }
case .randomError: return NSLocalizedString("A random error occured.", comment: "") case .randomError: return NSLocalizedString("A random error occured.", comment: "")
} }
} }
var errorUserInfo: [String: Any] { var errorUserInfo: [String : Any] {
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: errorDescription, let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription,
NSLocalizedFailureReasonErrorKey: failureReason, NSLocalizedFailureReasonErrorKey: self.failureReason,
NSLocalizedFailureErrorKey: failure, NSLocalizedFailureErrorKey: self.failure,
ErrorUserInfoKey.sourceFile: sourceFile, ErrorUserInfoKey.sourceFile: self.sourceFile,
ErrorUserInfoKey.sourceFileLine: sourceFileLine] ErrorUserInfoKey.sourceFileLine: self.sourceFileLine]
return userInfo.compactMapValues { $0 } return userInfo.compactMapValues { $0 }
} }
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line) { init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
{
self.code = code self.code = code
failure = description self.failure = description
sourceFile = file self.sourceFile = file
sourceFileLine = line self.sourceFileLine = line
} }
} }
class BackupController: NSObject { class BackupController: NSObject
{
private let fileCoordinator = NSFileCoordinator(filePresenter: nil) private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
private let operationQueue = OperationQueue() private let operationQueue = OperationQueue()
override init() { override init()
operationQueue.name = "AltBackup-BackupQueue" {
self.operationQueue.name = "AltBackup-BackupQueue"
} }
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) { func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
do { {
do
{
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else { guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: "")) throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
} }
@@ -101,24 +106,29 @@ class BackupController: NSObject {
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: []) let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing]) let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: operationQueue) { error in self.fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: self.operationQueue) { (error) in
do { do
if let error = error { {
if let error = error
{
throw error throw error
} }
do { do
{
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App") let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent) let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path) { if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path)
{
try FileManager.default.removeItem(at: backupDocumentsDirectory) try FileManager.default.removeItem(at: backupDocumentsDirectory)
} }
if FileManager.default.fileExists(atPath: documentsDirectory.path) { if FileManager.default.fileExists(atPath: documentsDirectory.path)
{
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory) try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
} }
@@ -127,18 +137,21 @@ class BackupController: NSObject {
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent) let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path) { if FileManager.default.fileExists(atPath: backupLibraryDirectory.path)
{
try FileManager.default.removeItem(at: backupLibraryDirectory) try FileManager.default.removeItem(at: backupLibraryDirectory)
} }
if FileManager.default.fileExists(atPath: libraryDirectory.path) { if FileManager.default.fileExists(atPath: libraryDirectory.path)
{
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory) try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
} }
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)") print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
} }
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup { for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: "")) throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
} }
@@ -152,22 +165,29 @@ class BackupController: NSObject {
// Replace previous backup with new backup. // Replace previous backup with new backup.
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory) _ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
os_log("Replaced previous backup with new backup: %@", type: .info, temporaryAppBackupDirectory.absoluteString) print("Replaced previous backup with new backup:", temporaryAppBackupDirectory)
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) } catch { os_log("Failed to remove temporary directory. %@", type: .error , error.localizedDescription) } catch
{
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) }
catch { print("Failed to remove temporary directory.", error) }
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) { func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
do { {
do
{
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else { guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: "")) throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
} }
@@ -181,9 +201,11 @@ class BackupController: NSObject {
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier) let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: []) let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
fileCoordinator.coordinate(with: [readingIntent], queue: operationQueue) { error in self.fileCoordinator.coordinate(with: [readingIntent], queue: self.operationQueue) { (error) in
do { do
if let error = error { {
if let error = error
{
throw error throw error
} }
@@ -198,7 +220,8 @@ class BackupController: NSObject {
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory) try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory) try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup { for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: "")) throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
} }
@@ -208,35 +231,46 @@ class BackupController: NSObject {
} }
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} }
private extension BackupController { private extension BackupController
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws { {
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws
{
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return } guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path) { if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path)
{
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
} }
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options) { for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options)
{
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent) let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
if FileManager.default.fileExists(atPath: destinationURL.path) { if FileManager.default.fileExists(atPath: destinationURL.path)
{
do { do {
try FileManager.default.removeItem(at: destinationURL) try FileManager.default.removeItem(at: destinationURL)
} catch CocoaError.fileWriteNoPermission where isDirectory { }
try copyDirectoryContents(at: fileURL, to: destinationURL, options: options) catch CocoaError.fileWriteNoPermission where isDirectory {
try self.copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
continue continue
} catch { }
catch {
print(error) print(error)
throw error throw error
} }
@@ -245,10 +279,12 @@ private extension BackupController {
do { do {
try FileManager.default.copyItem(at: fileURL, to: destinationURL) try FileManager.default.copyItem(at: fileURL, to: destinationURL)
print("Copied item from \(fileURL) to \(destinationURL)") print("Copied item from \(fileURL) to \(destinationURL)")
} catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" { }
catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
// Ignore errors for /Documents/Inbox // Ignore errors for /Documents/Inbox
os_log("Failed to copy Inbox directory: %@", type: .error , error.localizedDescription) print("Failed to copy Inbox directory:", error)
} catch { }
catch {
print(error) print(error)
throw error throw error
} }

View File

@@ -35,16 +35,6 @@
<string>altbackup</string> <string>altbackup</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>SideBackup General</string>
<key>CFBundleURLSchemes</key>
<array>
<string>sidebackup</string>
</array>
</dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>

View File

@@ -8,7 +8,8 @@
import UIKit import UIKit
extension UIColor { extension UIColor
{
static let altstoreBackground = UIColor(named: "Background")! static let altstoreBackground = UIColor(named: "Background")!
static let altstoreText = UIColor(named: "Text")! static let altstoreText = UIColor(named: "Text")!
} }

View 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
}
}

View File

@@ -10,24 +10,28 @@ import Foundation
import AltSign import AltSign
private extension UserDefaults { private extension UserDefaults
{
@objc var localUserID: String? { @objc var localUserID: String? {
get { string(forKey: #keyPath(UserDefaults.localUserID)) } get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
set { set(newValue, forKey: #keyPath(UserDefaults.localUserID)) } set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
} }
} }
struct AnisetteDataManager { struct AnisetteDataManager
{
static let shared = AnisetteDataManager() static let shared = AnisetteDataManager()
private let dateFormatter = ISO8601DateFormatter() private let dateFormatter = ISO8601DateFormatter()
private init() { private init()
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW) {
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
} }
func requestAnisetteData() throws -> ALTAnisetteData { func requestAnisetteData() throws -> ALTAnisetteData
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!) {
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
request.httpMethod = "POST" request.httpMethod = "POST"
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self) let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
@@ -37,10 +41,11 @@ struct AnisetteDataManager {
let headers = session.appleIDHeaders(for: request) let headers = session.appleIDHeaders(for: request)
let device = akDevice.current let device = akDevice.current
let date = dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date() let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
var localUserID = UserDefaults.standard.localUserID var localUserID = UserDefaults.standard.localUserID
if localUserID == nil { if localUserID == nil
{
localUserID = UUID().uuidString localUserID = UUID().uuidString
UserDefaults.standard.localUserID = localUserID UserDefaults.standard.localUserID = localUserID
} }

View File

@@ -10,15 +10,18 @@ import Foundation
import AltSign import AltSign
private extension URL { private extension URL
{
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true) static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
} }
private extension CFNotificationName { private extension CFNotificationName
{
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString) static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
} }
struct AppManager { struct AppManager
{
static let shared = AppManager() static let shared = AppManager()
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated) private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
@@ -26,24 +29,27 @@ struct AppManager {
private let fileCoordinator = NSFileCoordinator() private let fileCoordinator = NSFileCoordinator()
private init() { private init()
profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue" {
profilesQueue.qualityOfService = .userInitiated self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
self.profilesQueue.qualityOfService = .userInitiated
} }
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles _: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) { func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
appQueue.async { {
self.appQueue.async {
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String: Any] let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) } let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
completionHandler(result) completionHandler(result)
} }
} }
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void) { func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
appQueue.async { {
self.appQueue.async {
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil) lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
@@ -51,11 +57,14 @@ struct AppManager {
} }
} }
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) { func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: []) let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
do { do
if let error = error { {
if let error = error
{
throw error throw error
} }
@@ -64,24 +73,31 @@ struct AppManager {
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: []) let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
// Remove all inactive profiles (if active profiles are provided), and the previous profiles. // Remove all inactive profiles (if active profiles are provided), and the previous profiles.
for fileURL in profileURLs { for fileURL in profileURLs
{
// Use memory mapping to reduce peak memory usage and stay within limit. // Use memory mapping to reduce peak memory usage and stay within limit.
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue } guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile) { if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
{
try FileManager.default.removeItem(at: fileURL) try FileManager.default.removeItem(at: fileURL)
} else { }
os_log("Ignoring: %@ %@", type: .info , profile.bundleIdentifier, profile.uuid) else
{
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
} }
} }
for profile in profiles { for profile in profiles
{
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased()) let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
try profile.data.write(to: destinationURL, options: .atomic) try profile.data.write(to: destinationURL, options: .atomic)
} }
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
@@ -90,22 +106,28 @@ struct AppManager {
} }
} }
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void) { func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: []) let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
do { do
{
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: []) let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
for fileURL in profileURLs { for fileURL in profileURLs
{
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue } guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
if bundleIdentifiers.contains(profile.bundleIdentifier) { if bundleIdentifiers.contains(profile.bundleIdentifier)
{
try FileManager.default.removeItem(at: fileURL) try FileManager.default.removeItem(at: fileURL)
} }
} }
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }

View File

@@ -8,69 +8,75 @@
import Foundation import Foundation
import SideKit
import OSLog
#if canImport(Logging)
import Logging
#endif
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler> typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(), private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
connectionHandlers: [XPCConnectionHandler()]) connectionHandlers: [XPCConnectionHandler()])
extension DaemonConnectionManager { extension DaemonConnectionManager
{
static var shared: ConnectionManager { static var shared: ConnectionManager {
connectionManager return connectionManager
} }
} }
struct DaemonRequestHandler: RequestHandler { struct DaemonRequestHandler: RequestHandler
func handleAnisetteDataRequest(_: AnisetteDataRequest, for _: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void) { {
do { func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
{
do
{
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData() let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
let response = AnisetteDataResponse(anisetteData: anisetteData) let response = AnisetteDataResponse(anisetteData: anisetteData)
completionHandler(.success(response)) completionHandler(.success(response))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void) { func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
{
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) } guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
print("Awaiting begin installation request...") print("Awaiting begin installation request...")
connection.receiveRequest { result in connection.receiveRequest() { (result) in
os_log("Received begin installation request with result: %@", type: .info , String(describing: result)) print("Received begin installation request with result:", result)
do { do
guard case let .beginInstallation(request) = try result.get() else { throw ALTServerError(.unknownRequest) } {
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) } guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { result in AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
let result = result.map { InstallationProgressResponse(progress: 1.0) } let result = result.map { InstallationProgressResponse(progress: 1.0) }
os_log("Installed app with result: %@", type: .info, String(describing: result)) print("Installed app with result:", result)
completionHandler(result) completionHandler(result)
} }
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} }
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for _: Connection, func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void) { completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { result in {
switch result { AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
case let .failure(error): switch result
os_log("Failed to install profiles %@ : %@", type: .error , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n"), error.localizedDescription) {
case .failure(let error):
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success: case .success:
os_log("Installed profiles: %@", type: .info , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n")) print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
let response = InstallProvisioningProfilesResponse() let response = InstallProvisioningProfilesResponse()
completionHandler(.success(response)) completionHandler(.success(response))
@@ -78,16 +84,18 @@ struct DaemonRequestHandler: RequestHandler {
} }
} }
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for _: Connection, func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void) { completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { result in {
switch result { AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
case let .failure(error): switch result
os_log("Failed to remove profiles %@ : %@", type: .error, request.bundleIdentifiers, error.localizedDescription) {
case .failure(let error):
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success: case .success:
os_log("Removed profiles: %@", type: .info , request.bundleIdentifiers) print("Removed profiles:", request.bundleIdentifiers)
let response = RemoveProvisioningProfilesResponse() let response = RemoveProvisioningProfilesResponse()
completionHandler(.success(response)) completionHandler(.success(response))
@@ -95,15 +103,17 @@ struct DaemonRequestHandler: RequestHandler {
} }
} }
func handleRemoveAppRequest(_ request: RemoveAppRequest, for _: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void) { func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { result in {
switch result { AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
case let .failure(error): switch result
os_log("Failed to remove app %@ : %@", type: .error , request.bundleIdentifier, error.localizedDescription) {
case .failure(let error):
print("Failed to remove app \(request.bundleIdentifier):", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success: case .success:
os_log("Removed app: %@", type: .info , request.bundleIdentifier) print("Removed app:", request.bundleIdentifier)
let response = RemoveAppResponse() let response = RemoveAppResponse()
completionHandler(.success(response)) completionHandler(.success(response))

View File

@@ -8,41 +8,49 @@
import Foundation import Foundation
import Security import Security
import SideStoreCore
class XPCConnectionHandler: NSObject, ConnectionHandler { class XPCConnectionHandler: NSObject, ConnectionHandler
{
var connectionHandler: ((Connection) -> Void)? var connectionHandler: ((Connection) -> Void)?
var disconnectionHandler: ((Connection) -> Void)? var disconnectionHandler: ((Connection) -> Void)?
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility) private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) } private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
deinit { deinit
{
self.stopListening() self.stopListening()
} }
func startListening() { func startListening()
for listener in listeners { {
for listener in self.listeners
{
listener.delegate = self listener.delegate = self
listener.resume() listener.resume()
} }
} }
func stopListening() { func stopListening()
listeners.forEach { $0.suspend() } {
self.listeners.forEach { $0.suspend() }
} }
} }
private extension XPCConnectionHandler { private extension XPCConnectionHandler
func disconnect(_ connection: Connection) { {
func disconnect(_ connection: Connection)
{
connection.disconnect() connection.disconnect()
disconnectionHandler?(connection) self.disconnectionHandler?(connection)
} }
} }
extension XPCConnectionHandler: NSXPCListenerDelegate { extension XPCConnectionHandler: NSXPCListenerDelegate
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
{
let maximumPathLength = 4 * UInt32(MAXPATHLEN) let maximumPathLength = 4 * UInt32(MAXPATHLEN)
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength)) let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
@@ -78,7 +86,7 @@ extension XPCConnectionHandler: NSXPCListenerDelegate {
self.disconnect(connection) self.disconnect(connection)
} }
connectionHandler?(connection) self.connectionHandler?(connection)
return true return true
} }

3
AltStore.xcconfig Normal file
View File

@@ -0,0 +1,3 @@
#include "Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME)

View File

@@ -63,15 +63,6 @@
"version" : "1.10.1" "version" : "1.10.1"
} }
}, },
{
"identity" : "roxas",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JoeMatt/Roxas.git",
"state" : {
"revision" : "17338c09ec0ffeea4c68135f17c1f801a3d6d10d",
"version" : "1.2.2"
}
},
{ {
"identity" : "semanticversion", "identity" : "semanticversion",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -81,15 +72,6 @@
"version" : "0.3.5" "version" : "0.3.5"
} }
}, },
{
"identity" : "sidekit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SideStore/SideKit.git",
"state" : {
"revision" : "7ea34a09b52c104077dea8e0b90f8dc55d43b36b",
"version" : "0.1.0"
}
},
{ {
"identity" : "sparkle", "identity" : "sparkle",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -31,7 +31,7 @@
BlueprintIdentifier = "BF1E314F22A0616100370A3C" BlueprintIdentifier = "BF1E314F22A0616100370A3C"
BuildableName = "libAltKit.a" BuildableName = "libAltKit.a"
BlueprintName = "AltKit" BlueprintName = "AltKit"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
<BuildActionEntry <BuildActionEntry
@@ -43,9 +43,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981" BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "SideDaemon" BuildableName = "AltDaemon"
BlueprintName = "SideDaemon" BlueprintName = "AltDaemon"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -72,9 +72,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981" BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "SideDaemon" BuildableName = "AltDaemon"
BlueprintName = "SideDaemon" BlueprintName = "AltDaemon"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<EnvironmentVariables> <EnvironmentVariables>
@@ -95,9 +95,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981" BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "SideDaemon" BuildableName = "AltDaemon"
BlueprintName = "SideDaemon" BlueprintName = "AltDaemon"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6" BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle" BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin" BlueprintName = "AltPlugin"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -53,7 +53,7 @@
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6" BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle" BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin" BlueprintName = "AltPlugin"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -27,17 +27,19 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables> <AdditionalOptions>
</Testables> </AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -56,9 +58,11 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -73,7 +77,7 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -47,7 +47,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments> <CommandLineArguments>
@@ -70,7 +70,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -47,7 +47,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments> <CommandLineArguments>
@@ -70,7 +70,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BFF7C903257844C900E55F36" BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc" BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC" BlueprintName = "AltXPC"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -47,7 +47,7 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</LaunchAction> </LaunchAction>
@@ -63,7 +63,7 @@
BlueprintIdentifier = "BFF7C903257844C900E55F36" BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc" BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC" BlueprintName = "AltXPC"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View 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"

View File

@@ -4,8 +4,6 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/>
<key>com.apple.developer.siri</key> <key>com.apple.developer.siri</key>
<true/> <true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>

View File

@@ -8,7 +8,7 @@
import Foundation import Foundation
import SideStoreCore import AltStoreCore
import AppCenter import AppCenter
import AppCenterAnalytics import AppCenterAnalytics
@@ -16,8 +16,10 @@ import AppCenterCrashes
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44" private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
public extension AnalyticsManager { extension AnalyticsManager
enum EventProperty: String { {
enum EventProperty: String
{
case name case name
case bundleIdentifier case bundleIdentifier
case developerName case developerName
@@ -28,24 +30,27 @@ public extension AnalyticsManager {
case sourceURL case sourceURL
} }
enum Event { enum Event
{
case installedApp(InstalledApp) case installedApp(InstalledApp)
case updatedApp(InstalledApp) case updatedApp(InstalledApp)
case refreshedApp(InstalledApp) case refreshedApp(InstalledApp)
public var name: String { var name: String {
switch self { switch self
{
case .installedApp: return "installed_app" case .installedApp: return "installed_app"
case .updatedApp: return "updated_app" case .updatedApp: return "updated_app"
case .refreshedApp: return "refreshed_app" case .refreshedApp: return "refreshed_app"
} }
} }
public var properties: [EventProperty: String] { var properties: [EventProperty: String] {
let properties: [EventProperty: String?] let properties: [EventProperty: String?]
switch self { switch self
case let .installedApp(app), let .updatedApp(app), let .refreshedApp(app): {
case .installedApp(let app), .updatedApp(let app), .refreshedApp(let app):
let appBundleURL = InstalledApp.fileURL(for: app) let appBundleURL = InstalledApp.fileURL(for: app)
let appBundleSize = FileManager.default.directorySize(at: appBundleURL) let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
@@ -66,22 +71,28 @@ public extension AnalyticsManager {
} }
} }
public final class AnalyticsManager { final class AnalyticsManager
public static let shared = AnalyticsManager() {
static let shared = AnalyticsManager()
private init() {} private init()
{
}
} }
public extension AnalyticsManager { extension AnalyticsManager
func start() { {
func start()
{
AppCenter.start(withAppSecret: appCenterAppSecret, services: [ AppCenter.start(withAppSecret: appCenterAppSecret, services: [
Analytics.self, Analytics.self,
Crashes.self Crashes.self
]) ])
} }
func trackEvent(_ event: Event) { func trackEvent(_ event: Event)
let properties = event.properties.reduce(into: [:]) { properties, item in {
let properties = event.properties.reduce(into: [:]) { (properties, item) in
properties[item.key.rawValue] = item.value properties[item.key.rawValue] = item.value
} }

View File

@@ -8,17 +8,15 @@
import UIKit import UIKit
import SideStoreCore import AltStoreCore
import RoxasUIKit import Roxas
import OSLog
#if canImport(Logging)
import Logging
#endif
import Nuke import Nuke
extension AppContentViewController { extension AppContentViewController
private enum Row: Int, CaseIterable { {
private enum Row: Int, CaseIterable
{
case subtitle case subtitle
case screenshots case screenshots
case description case description
@@ -27,7 +25,8 @@ extension AppContentViewController {
} }
} }
final class AppContentViewController: UITableViewController { final class AppContentViewController: UITableViewController
{
var app: StoreApp! var app: StoreApp!
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource() private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
@@ -68,56 +67,62 @@ final class AppContentViewController: UITableViewController {
return CGSize(width: itemWidth, height: itemHeight) return CGSize(width: itemWidth, height: itemHeight)
} }
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
tableView.contentInset.bottom = 20 self.tableView.contentInset.bottom = 20
screenshotsCollectionView.dataSource = screenshotsDataSource self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
screenshotsCollectionView.prefetchDataSource = screenshotsDataSource self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
permissionsCollectionView.dataSource = permissionsDataSource self.permissionsCollectionView.dataSource = self.permissionsDataSource
subtitleLabel.text = app.subtitle self.subtitleLabel.text = self.app.subtitle
descriptionTextView.text = app.localizedDescription self.descriptionTextView.text = self.app.localizedDescription
if let version = app.latestVersion { if let version = self.app.latestVersion
versionDescriptionTextView.text = version.localizedDescription {
versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version) self.versionDescriptionTextView.text = version.localizedDescription
versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: dateFormatter) self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
sizeLabel.text = byteCountFormatter.string(fromByteCount: version.size) self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
} else { self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
versionDescriptionTextView.text = nil }
versionLabel.text = nil else
versionDateLabel.text = nil {
sizeLabel.text = byteCountFormatter.string(fromByteCount: 0) self.versionDescriptionTextView.text = nil
self.versionLabel.text = nil
self.versionDateLabel.text = nil
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
} }
descriptionTextView.maximumNumberOfLines = 5 self.descriptionTextView.maximumNumberOfLines = 5
descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
versionDescriptionTextView.maximumNumberOfLines = 3 self.versionDescriptionTextView.maximumNumberOfLines = 3
versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
} }
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
guard var size = preferredScreenshotSize else { return } guard var size = self.preferredScreenshotSize else { return }
size.height = min(size.height, screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning. size.height = min(size.height, self.screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
let layout = screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = size layout.itemSize = size
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard segue.identifier == "showPermission" else { return } guard segue.identifier == "showPermission" else { return }
guard let cell = sender as? UICollectionViewCell, let indexPath = permissionsCollectionView.indexPath(for: cell) else { return } guard let cell = sender as? UICollectionViewCell, let indexPath = self.permissionsCollectionView.indexPath(for: cell) else { return }
let permission = permissionsDataSource.item(at: indexPath) let permission = self.permissionsDataSource.item(at: indexPath)
let maximumWidth = view.bounds.width - 20 let maximumWidth = self.view.bounds.width - 20
let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController
permissionPopoverViewController.permission = permission permissionPopoverViewController.permission = permission
@@ -128,48 +133,55 @@ final class AppContentViewController: UITableViewController {
permissionPopoverViewController.popoverPresentationController?.delegate = self permissionPopoverViewController.popoverPresentationController?.delegate = self
permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame
permissionPopoverViewController.popoverPresentationController?.sourceView = permissionsCollectionView permissionPopoverViewController.popoverPresentationController?.sourceView = self.permissionsCollectionView
} }
} }
private extension AppContentViewController { private extension AppContentViewController
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> { {
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: app.screenshotURLs as [NSURL]) func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
dataSource.cellConfigurationHandler = { cell, _, _ in {
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.image = nil cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true cell.imageView.isIndicatingActivity = true
} }
dataSource.prefetchHandler = { imageURL, _, completionHandler in dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
RSTAsyncBlockOperation { operation in return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL as URL, processor: .screenshot) let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() } guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image { if let image = response?.image
{
completionHandler(image, nil) completionHandler(image, nil)
} else { }
else
{
completionHandler(nil, error) completionHandler(nil, error)
} }
}) })
} }
} }
dataSource.prefetchCompletionHandler = { cell, image, _, error in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false cell.imageView.isIndicatingActivity = false
cell.imageView.image = image cell.imageView.image = image
if let error = error { if let error = error
os_log("Error loading image: %@", type: .error, error.localizedDescription) {
print("Error loading image:", error)
} }
} }
return dataSource return dataSource
} }
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission> { func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
let dataSource = RSTArrayCollectionViewDataSource(items: app.permissions) {
dataSource.cellConfigurationHandler = { cell, permission, _ in let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions)
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
let cell = cell as! PermissionCollectionViewCell let cell = cell as! PermissionCollectionViewCell
cell.button.setImage(permission.type.icon, for: .normal) cell.button.setImage(permission.type.icon, for: .normal)
cell.button.tintColor = .label cell.button.tintColor = .label
@@ -180,13 +192,16 @@ private extension AppContentViewController {
} }
} }
private extension AppContentViewController { private extension AppContentViewController
@objc func toggleCollapsingSection(_ sender: UIButton) { {
@objc func toggleCollapsingSection(_ sender: UIButton)
{
let indexPath: IndexPath let indexPath: IndexPath
switch sender { switch sender
case descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0) {
case versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0) case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
default: return default: return
} }
@@ -197,19 +212,23 @@ private extension AppContentViewController {
} }
} }
extension AppContentViewController { extension AppContentViewController
override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt _: IndexPath) { {
cell.tintColor = app.tintColor override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
{
cell.tintColor = self.app.tintColor
} }
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
switch Row.allCases[indexPath.row] { {
switch Row.allCases[indexPath.row]
{
case .screenshots: case .screenshots:
guard let size = preferredScreenshotSize else { return 0.0 } guard let size = self.preferredScreenshotSize else { return 0.0 }
return size.height return size.height
case .permissions: case .permissions:
guard !app.permissions.isEmpty else { return 0.0 } guard !self.app.permissions.isEmpty else { return 0.0 }
return super.tableView(tableView, heightForRowAt: indexPath) return super.tableView(tableView, heightForRowAt: indexPath)
default: default:
@@ -218,8 +237,10 @@ extension AppContentViewController {
} }
} }
extension AppContentViewController: UIPopoverPresentationControllerDelegate { extension AppContentViewController: UIPopoverPresentationControllerDelegate
func adaptivePresentationStyle(for _: UIPresentationController, traitCollection _: UITraitCollection) -> UIModalPresentationStyle { {
.none func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
{
return .none
} }
} }

View File

@@ -8,30 +8,33 @@
import UIKit import UIKit
@objc final class PermissionCollectionViewCell: UICollectionViewCell
final class PermissionCollectionViewCell: UICollectionViewCell { {
@IBOutlet var button: UIButton! @IBOutlet var button: UIButton!
@IBOutlet var textLabel: UILabel! @IBOutlet var textLabel: UILabel!
override func layoutSubviews() { override func layoutSubviews()
{
super.layoutSubviews() super.layoutSubviews()
button.layer.cornerRadius = button.bounds.midY self.button.layer.cornerRadius = self.button.bounds.midY
} }
override func tintColorDidChange() { override func tintColorDidChange()
{
super.tintColorDidChange() super.tintColorDidChange()
button.backgroundColor = tintColor.withAlphaComponent(0.15) self.button.backgroundColor = self.tintColor.withAlphaComponent(0.15)
textLabel.textColor = tintColor self.textLabel.textColor = self.tintColor
} }
} }
@objc final class AppContentTableViewCell: UITableViewCell
final class AppContentTableViewCell: UITableViewCell { {
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
{
// Ensure cell is laid out so it will report correct size. // Ensure cell is laid out so it will report correct size.
layoutIfNeeded() self.layoutIfNeeded()
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority) let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)

View 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()
}
}

View File

@@ -8,18 +8,20 @@
import UIKit import UIKit
import SideStoreCore import AltStoreCore
final class PermissionPopoverViewController: UIViewController { final class PermissionPopoverViewController: UIViewController
{
var permission: AppPermission! var permission: AppPermission!
@IBOutlet private var nameLabel: UILabel! @IBOutlet private var nameLabel: UILabel!
@IBOutlet private var descriptionLabel: UILabel! @IBOutlet private var descriptionLabel: UILabel!
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
nameLabel.text = permission.type.localizedName self.nameLabel.text = self.permission.type.localizedName
descriptionLabel.text = permission.usageDescription self.descriptionLabel.text = self.permission.usageDescription
} }
} }

View 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()
}
}
}

View File

@@ -6,23 +6,32 @@
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import AVFoundation
import Intents
import UIKit import UIKit
import UserNotifications import UserNotifications
import OSLog import AVFoundation
#if canImport(Logging) import Intents
import Logging
#endif
import AltStoreCore
import AltSign import AltSign
import SideStoreCore import Roxas
import SideStoreAppKit
import EmotionalDamage import EmotionalDamage
import RoxasUIKit
extension AppDelegate
{
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
static let importAppDeepLinkURLKey = "fileURL"
static let appBackupResultKey = "result"
static let addSourceDeepLinkURLKey = "sourceURL"
}
@UIApplicationMain @UIApplicationMain
final class AppDelegate: SideStoreAppDelegate { final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
@available(iOS 14, *) @available(iOS 14, *)
@@ -47,27 +56,30 @@ final class AppDelegate: SideStoreAppDelegate {
return ViewAppIntentHandler() return ViewAppIntentHandler()
}() }()
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Register default settings before doing anything else. // Register default settings before doing anything else.
UserDefaults.registerDefaults() UserDefaults.registerDefaults()
DatabaseManager.shared.start { error in DatabaseManager.shared.start { (error) in
if let error = error { if let error = error
os_log("Failed to start DatabaseManager. Error: %@", type: .error , error.localizedDescription) {
} else { print("Failed to start DatabaseManager. Error:", error as Any)
os_log("Started DatabaseManager.", type: .info) }
let transformer = ALTAppPermissionTypeTransformer() else
ValueTransformer.setValueTransformer(transformer, forName: NSValueTransformerName(rawValue: "ALTAppPermissionTypeTransformer")) {
print("Started DatabaseManager.")
} }
} }
AnalyticsManager.shared.start() AnalyticsManager.shared.start()
setTintColor() self.setTintColor()
SecureValueTransformer.register() SecureValueTransformer.register()
if UserDefaults.standard.firstLaunch == nil { if UserDefaults.standard.firstLaunch == nil
{
Keychain.shared.reset() Keychain.shared.reset()
UserDefaults.standard.firstLaunch = Date() UserDefaults.standard.firstLaunch = Date()
} }
@@ -75,70 +87,84 @@ final class AppDelegate: SideStoreAppDelegate {
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
#if DEBUG || BETA #if DEBUG || BETA
UserDefaults.standard.isDebugModeEnabled = true UserDefaults.standard.isDebugModeEnabled = true
#endif #endif
prepareForBackgroundFetch() self.prepareForBackgroundFetch()
return true return true
} }
func applicationDidEnterBackground(_: UIApplication) { func applicationDidEnterBackground(_ application: UIApplication)
{
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well. // Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return } guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo) let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
switch result { switch result
{
case .success: break case .success: break
case let .failure(error): os_log("[ALTLog] Failed to purge logged errors before %@. %@", type: .error , midnightOneMonthAgo.debugDescription, error.localizedDescription) case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
} }
} }
} }
func applicationWillEnterForeground(_: UIApplication) { func applicationWillEnterForeground(_ application: UIApplication)
{
AppManager.shared.update() AppManager.shared.update()
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
} }
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
open(url) {
return self.open(url)
} }
func application(_: UIApplication, handlerFor intent: INIntent) -> Any? { func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
{
guard #available(iOS 14, *) else { return nil } guard #available(iOS 14, *) else { return nil }
switch intent { switch intent
case is RefreshAllIntent: return intentHandler {
case is ViewAppIntent: return viewAppIntentHandler case is RefreshAllIntent: return self.intentHandler
case is ViewAppIntent: return self.viewAppIntentHandler
default: return nil default: return nil
} }
} }
} }
@available(iOS 13, *) @available(iOS 13, *)
extension AppDelegate { extension AppDelegate
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
// Called when a new scene session is being created. // Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with. // Use this method to select a configuration to create the new scene with.
UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
} }
func application(_: UIApplication, didDiscardSceneSessions _: Set<UISceneSession>) { func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
{
// Called when the user discards a scene session. // Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return. // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
} }
} }
private extension AppDelegate { private extension AppDelegate
func setTintColor() { {
window?.tintColor = .altPrimary func setTintColor()
{
self.window?.tintColor = .altPrimary
} }
func open(_ url: URL) -> Bool { func open(_ url: URL) -> Bool
if url.isFileURL { {
if url.isFileURL
{
guard url.pathExtension.lowercased() == "ipa" else { return false } guard url.pathExtension.lowercased() == "ipa" else { return false }
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -146,11 +172,14 @@ private extension AppDelegate {
} }
return true return true
} else { }
else
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let host = components.host?.lowercased() else { return false } guard let host = components.host?.lowercased() else { return false }
switch host { switch host
{
case "patreon": case "patreon":
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
@@ -161,7 +190,8 @@ private extension AppDelegate {
case "appbackupresponse": case "appbackupresponse":
let result: Result<Void, Error> let result: Result<Void, Error>
switch url.path.lowercased() { switch url.path.lowercased()
{
case "/success": result = .success(()) case "/success": result = .success(())
case "/failure": case "/failure":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:] let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
@@ -207,35 +237,40 @@ private extension AppDelegate {
} }
} }
extension AppDelegate { extension AppDelegate
private func prepareForBackgroundFetch() { {
private func prepareForBackgroundFetch()
{
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery). // "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60) UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
} }
#if DEBUG #if DEBUG
UIApplication.shared.registerForRemoteNotifications() UIApplication.shared.registerForRemoteNotifications()
#endif #endif
} }
func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
{ {
let tokenParts = deviceToken.map { data -> String in let tokenParts = deviceToken.map { data -> String in
String(format: "%02.2hhx", data) return String(format: "%02.2hhx", data)
} }
let token = tokenParts.joined() let token = tokenParts.joined()
os_log("Push Token: %@", type: .debug , token) print("Push Token:", token)
} }
func application(_ application: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
self.application(application, performFetchWithCompletionHandler: completionHandler) self.application(application, performFetchWithCompletionHandler: completionHandler)
} }
func application(_: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) { func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification { {
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
{
let threeHours: TimeInterval = 3 * 60 * 60 let threeHours: TimeInterval = 3 * 60 * 60
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
@@ -249,31 +284,38 @@ extension AppDelegate {
UserDefaults.standard.presentedLaunchReminderNotification = true UserDefaults.standard.presentedLaunchReminderNotification = true
} }
BackgroundTaskManager.shared.performExtendedBackgroundTask { taskResult, taskCompletionHandler in BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
if let error = taskResult.error { if let error = taskResult.error
os_log("Error starting extended background task. Aborting. %@", type: .error, error.localizedDescription) {
print("Error starting extended background task. Aborting.", error)
backgroundFetchCompletionHandler(.failed) backgroundFetchCompletionHandler(.failed)
taskCompletionHandler() taskCompletionHandler()
return return
} }
if !DatabaseManager.shared.isStarted { if !DatabaseManager.shared.isStarted
DatabaseManager.shared.start { error in {
if error != nil { DatabaseManager.shared.start() { (error) in
if error != nil
{
backgroundFetchCompletionHandler(.failed) backgroundFetchCompletionHandler(.failed)
taskCompletionHandler() taskCompletionHandler()
} else { }
self.performBackgroundFetch { backgroundFetchResult in else
{
self.performBackgroundFetch { (backgroundFetchResult) in
backgroundFetchCompletionHandler(backgroundFetchResult) backgroundFetchCompletionHandler(backgroundFetchResult)
} refreshAppsCompletionHandler: { _ in } refreshAppsCompletionHandler: { (refreshAppsResult) in
taskCompletionHandler() taskCompletionHandler()
} }
} }
} }
} else { }
self.performBackgroundFetch { backgroundFetchResult in else
{
self.performBackgroundFetch { (backgroundFetchResult) in
backgroundFetchCompletionHandler(backgroundFetchResult) backgroundFetchCompletionHandler(backgroundFetchResult)
} refreshAppsCompletionHandler: { _ in } refreshAppsCompletionHandler: { (refreshAppsResult) in
taskCompletionHandler() taskCompletionHandler()
} }
} }
@@ -281,31 +323,37 @@ extension AppDelegate {
} }
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void, func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) { refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
fetchSources { result in {
switch result { self.fetchSources { (result) in
switch result
{
case .failure: backgroundFetchCompletionHandler(.failed) case .failure: backgroundFetchCompletionHandler(.failed)
case .success: backgroundFetchCompletionHandler(.newData) case .success: backgroundFetchCompletionHandler(.newData)
} }
if !UserDefaults.standard.isBackgroundRefreshEnabled { if !UserDefaults.standard.isBackgroundRefreshEnabled
{
refreshAppsCompletionHandler(.success([:])) refreshAppsCompletionHandler(.success([:]))
} }
} }
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return } guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context) let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler) AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
} }
} }
} }
private extension AppDelegate { private extension AppDelegate
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void) { {
AppManager.shared.fetchSources { result in func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
do { {
AppManager.shared.fetchSources() { (result) in
do
{
let (sources, context) = try result.get() let (sources, context) = try result.get()
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult> let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
@@ -329,7 +377,8 @@ private extension AppDelegate {
let updates = try context.fetch(updatesFetchRequest) let updates = try context.fetch(updatesFetchRequest)
let newsItems = try context.fetch(newsItemsFetchRequest) let newsItems = try context.fetch(newsItemsFetchRequest)
for update in updates { for update in updates
{
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue } guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
guard let storeApp = update.storeApp, let version = storeApp.version else { continue } guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
@@ -342,15 +391,19 @@ private extension AppDelegate {
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
} }
for newsItem in newsItems { for newsItem in newsItems
{
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue } guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
guard !newsItem.isSilent else { continue } guard !newsItem.isSilent else { continue }
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
if let app = newsItem.storeApp { if let app = newsItem.storeApp
{
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name) content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
} else { }
else
{
content.title = NSLocalizedString("SideStore News", comment: "") content.title = NSLocalizedString("SideStore News", comment: "")
} }
@@ -366,8 +419,10 @@ private extension AppDelegate {
} }
completionHandler(.success(sources)) completionHandler(.success(sources))
} catch { }
os_log("Error fetching apps: %@", type: .error, error.localizedDescription) catch
{
print("Error fetching apps:", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -12,9 +12,9 @@
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="lNR-II-WoW"> <scene sceneID="lNR-II-WoW">
<objects> <objects>
<navigationController storyboardIdentifier="navigationController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="ZTo-53-dSL" sceneMemberID="viewController"> <navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="59" width="393" height="96"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/> <color key="barTintColor" name="SettingsBackground"/>
@@ -36,34 +36,34 @@
<!--Authentication View Controller--> <!--Authentication View Controller-->
<scene sceneID="OCd-xc-Ms7"> <scene sceneID="OCd-xc-Ms7">
<objects> <objects>
<viewController storyboardIdentifier="authenticationViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH"> <view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View"> <view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
<rect key="frame" x="0.0" y="103" width="393" height="715"/> <rect key="frame" x="0.0" y="44" width="375" height="623"/>
</view> </view>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv"> <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews> <subviews>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z"> <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
<rect key="frame" x="0.0" y="0.0" width="393" height="715"/> <rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh"> <stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
<rect key="frame" x="16" y="6" width="361" height="359"/> <rect key="frame" x="16" y="6" width="343" height="359.5"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
<rect key="frame" x="0.0" y="0.0" width="361" height="67"/> <rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
<rect key="frame" x="0.0" y="0.0" width="357.33333333333331" height="40.666666666666664"/> <rect key="frame" x="0.0" y="0.0" width="332" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
<rect key="frame" x="0.0" y="46.666666666666664" width="306.33333333333331" height="20.333333333333336"/> <rect key="frame" x="0.0" y="47" width="306.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -71,19 +71,19 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf">
<rect key="frame" x="0.0" y="117" width="361" height="242"/> <rect key="frame" x="0.0" y="117.5" width="343" height="242"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7">
<rect key="frame" x="0.0" y="0.0" width="361" height="159"/> <rect key="frame" x="0.0" y="0.0" width="343" height="159"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID">
<rect key="frame" x="0.0" y="0.0" width="361" height="72"/> <rect key="frame" x="0.0" y="0.0" width="343" height="72"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q">
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/> <rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="APPLE ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="59N-O1-6bM"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="APPLE ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="59N-O1-6bM">
<rect key="frame" x="14" y="0.0" width="347" height="17"/> <rect key="frame" x="14" y="0.0" width="329" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -92,10 +92,10 @@
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/> <edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
</stackView> </stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1">
<rect key="frame" x="0.0" y="21" width="361" height="51"/> <rect key="frame" x="0.0" y="21" width="343" height="51"/>
<subviews> <subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo"> <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo">
<rect key="frame" x="14" y="0.0" width="333" height="51"/> <rect key="frame" x="14" y="0.0" width="315" height="51"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
@@ -118,13 +118,13 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password">
<rect key="frame" x="0.0" y="87" width="361" height="72"/> <rect key="frame" x="0.0" y="87" width="343" height="72"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95">
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/> <rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PASSWORD" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ava-XY-7vs"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PASSWORD" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ava-XY-7vs">
<rect key="frame" x="14" y="0.0" width="347" height="17"/> <rect key="frame" x="14" y="0.0" width="329" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -133,10 +133,10 @@
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/> <edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
</stackView> </stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5">
<rect key="frame" x="0.0" y="21" width="361" height="51"/> <rect key="frame" x="0.0" y="21" width="343" height="51"/>
<subviews> <subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT"> <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT">
<rect key="frame" x="14" y="0.0" width="333" height="51"/> <rect key="frame" x="14" y="0.0" width="315" height="51"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
@@ -161,7 +161,7 @@
</subviews> </subviews>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
<rect key="frame" x="0.0" y="191" width="361" height="51"/> <rect key="frame" x="0.0" y="191" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/> <constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
@@ -179,16 +179,16 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8"> <stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
<rect key="frame" x="16" y="611" width="361" height="96"/> <rect key="frame" x="16" y="518.5" width="343" height="96.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
<rect key="frame" x="0.0" y="0.0" width="361" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY">
<rect key="frame" x="0.0" y="24.333333333333371" width="361" height="71.666666666666671"/> <rect key="frame" x="0.0" y="24.5" width="343" height="72"/>
<string key="text">Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.</string> <string key="text">Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.</string>
<fontDescription key="fontDescription" type="system" pointSize="15"/> <fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -258,19 +258,19 @@
<!--How it works--> <!--How it works-->
<scene sceneID="dMt-EA-SGy"> <scene sceneID="dMt-EA-SGy">
<objects> <objects>
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS"> <view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2"> <stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
<rect key="frame" x="0.0" y="103" width="393" height="656"/> <rect key="frame" x="0.0" y="44" width="375" height="564"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
<rect key="frame" x="16" y="35.000000000000007" width="361" height="95.666666666666686"/> <rect key="frame" x="16" y="35" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-3h-B8f"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-3h-B8f">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/> <constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/>
</constraints> </constraints>
@@ -279,16 +279,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0">
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/> <rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0">
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH">
<rect key="frame" x="0.0" y="25.333333333333346" width="282" height="38.333333333333343"/> <rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -298,10 +298,10 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
<rect key="frame" x="16" y="198.33333333333331" width="361" height="95.666666666666686"/> <rect key="frame" x="16" y="168" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/> <constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/>
</constraints> </constraints>
@@ -310,16 +310,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
<rect key="frame" x="79" y="16.333333333333371" width="282" height="63"/> <rect key="frame" x="79" y="17" width="264" height="61.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
<rect key="frame" x="0.0" y="25.333333333333311" width="282" height="37.666666666666657"/> <rect key="frame" x="0.0" y="25.5" width="264" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -329,10 +329,10 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
<rect key="frame" x="16" y="362" width="361" height="95.666666666666686"/> <rect key="frame" x="16" y="300.5" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/> <constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/>
</constraints> </constraints>
@@ -341,16 +341,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/> <rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt">
<rect key="frame" x="0.0" y="25.333333333333318" width="282" height="38.333333333333343"/> <rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -360,10 +360,10 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
<rect key="frame" x="16" y="525.33333333333337" width="361" height="95.666666666666629"/> <rect key="frame" x="16" y="433.5" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/> <constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/>
</constraints> </constraints>
@@ -372,16 +372,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/> <rect key="frame" x="79" y="17" width="264" height="62"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
<rect key="frame" x="0.0" y="25.333333333333261" width="282" height="38.333333333333343"/> <rect key="frame" x="0.0" y="25.5" width="264" height="36.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -394,7 +394,7 @@
<edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/> <edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
<rect key="frame" x="16" y="759" width="361" height="51"/> <rect key="frame" x="16" y="608" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/> <constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
@@ -431,22 +431,22 @@
</objects> </objects>
<point key="canvasLocation" x="1353" y="736"/> <point key="canvasLocation" x="1353" y="736"/>
</scene> </scene>
<!--Refresh SideStore--> <!--Refresh AltStore-->
<scene sceneID="9Vh-dM-OqX"> <scene sceneID="9Vh-dM-OqX">
<objects> <objects>
<viewController storyboardIdentifier="refreshAltStoreViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365"> <view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
</view> </view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
<rect key="frame" x="16" y="721" width="361" height="89"/> <rect key="frame" x="16" y="570" width="343" height="89"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="SideStore" customModuleProvider="target"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="361" height="51"/> <rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/> <constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/>
@@ -461,7 +461,7 @@
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
<rect key="frame" x="0.0" y="59" width="361" height="30"/> <rect key="frame" x="0.0" y="59" width="343" height="30"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" title="Refresh Later"> <state key="normal" title="Refresh Later">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -485,7 +485,7 @@
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/> <constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" title="Refresh SideStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/> <navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/> <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections> <connections>
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/> <outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
@@ -498,30 +498,30 @@
<!--Select a Team--> <!--Select a Team-->
<scene sceneID="ioQ-WB-CLJ"> <scene sceneID="ioQ-WB-CLJ">
<objects> <objects>
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="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"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" name="SettingsBackground"/> <color key="backgroundColor" name="SettingsBackground"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="55.333332061767578" width="393" height="60"/> <rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I">
<rect key="frame" x="0.0" y="0.0" width="352.66666666666669" height="60"/> <rect key="frame" x="0.0" y="0.0" width="334" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM">
<rect key="frame" x="30.000000000000004" y="9.9999999999999982" width="56.333333333333336" height="20.333333333333332"/> <rect key="frame" x="30" y="10" width="56.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf">
<rect key="frame" x="30" y="33.333333333333329" width="70" height="14.333333333333334"/> <rect key="frame" x="30" y="33.5" width="70" height="14.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -551,18 +551,19 @@
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1401" y="734"/> <point key="canvasLocation" x="1401" y="734"/>
</scene> </scene>
</scenes> </scenes>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
<resources> <resources>
<namedColor name="Primary"> <namedColor name="Primary">
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="SettingsBackground"> <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>
<namedColor name="SettingsHighlighted"> <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> </namedColor>
</resources> </resources>
</document> </document>

View 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()
}
}

View 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?()
}
}

View File

@@ -8,54 +8,61 @@
import UIKit import UIKit
import AltStoreCore
import AltSign import AltSign
import SideStoreCore import Roxas
import RoxasUIKit
final class RefreshAltStoreViewController: UIViewController { final class RefreshAltStoreViewController: UIViewController
{
var context: AuthenticatedOperationContext! var context: AuthenticatedOperationContext!
var completionHandler: ((Result<Void, Error>) -> Void)? var completionHandler: ((Result<Void, Error>) -> Void)?
@IBOutlet private var placeholderView: RSTPlaceholderView! @IBOutlet private var placeholderView: RSTPlaceholderView!
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
placeholderView.textLabel.isHidden = true self.placeholderView.textLabel.isHidden = true
placeholderView.detailTextLabel.textAlignment = .left self.placeholderView.detailTextLabel.textAlignment = .left
placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6) self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "") self.placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "")
} }
} }
private extension RefreshAltStoreViewController { private extension RefreshAltStoreViewController
@IBAction func refreshAltStore(_ sender: PillButton) { {
@IBAction func refreshAltStore(_ sender: PillButton)
{
guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return } guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return }
func refresh() { func refresh()
{
sender.isIndicatingActivity = true sender.isIndicatingActivity = true
if let progress = AppManager.shared.installationProgress(for: altStore) { if let progress = AppManager.shared.installationProgress(for: altStore)
{
// Cancel pending AltStore installation so we can start a new one. // Cancel pending AltStore installation so we can start a new one.
progress.cancel() progress.cancel()
} }
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate. // Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
let group = AppManager.shared.install(altStore, presentingViewController: self, context: context) { result in let group = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
switch result { switch result
{
case .success: self.completionHandler?(.success(())) case .success: self.completionHandler?(.success(()))
case let .failure(error as NSError): case .failure(let error as NSError):
DispatchQueue.main.async { DispatchQueue.main.async {
sender.progress = nil sender.progress = nil
sender.isIndicatingActivity = false sender.isIndicatingActivity = false
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert) let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { _ in alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in
refresh() refresh()
})) }))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { _ in alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { (action) in
self.completionHandler?(.failure(error)) self.completionHandler?(.failure(error))
})) }))
@@ -70,7 +77,8 @@ private extension RefreshAltStoreViewController {
refresh() refresh()
} }
@IBAction func cancel(_: UIButton) { @IBAction func cancel(_ sender: UIButton)
completionHandler?(.failure(OperationError.cancelled)) {
self.completionHandler?(.failure(OperationError.cancelled))
} }
} }

View 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"
}
}

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="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"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21204"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -13,7 +13,7 @@
<!--Launch View Controller--> <!--Launch View Controller-->
<scene sceneID="q24-yd-v7v"> <scene sceneID="q24-yd-v7v">
<objects> <objects>
<viewController storyboardIdentifier="launchViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM"> <view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -28,7 +28,7 @@
<!--Tab Bar Controller--> <!--Tab Bar Controller-->
<scene sceneID="yl2-sM-qoP"> <scene sceneID="yl2-sM-qoP">
<objects> <objects>
<tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" useStoryboardIdentifierAsRestorationIdentifier="YES" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA"> <tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
<rect key="frame" x="0.0" y="975" width="768" height="49"/> <rect key="frame" x="0.0" y="975" width="768" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
@@ -50,7 +50,7 @@
<!--Browse--> <!--Browse-->
<scene sceneID="rXq-UR-qQp"> <scene sceneID="rXq-UR-qQp">
<objects> <objects>
<collectionViewController storyboardIdentifier="browseViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -85,7 +85,7 @@
<!--App View Controller--> <!--App View Controller-->
<scene sceneID="TgT-LO-3Er"> <scene sceneID="TgT-LO-3Er">
<objects> <objects>
<viewController storyboardIdentifier="appViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="appViewController" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="0cR-li-tCB"> <view key="view" contentMode="scaleToFill" id="0cR-li-tCB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -131,7 +131,7 @@
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews> <subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="37" y="287" width="300" height="93"/> <rect key="frame" x="37" y="287" width="300" height="93"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view> </view>
@@ -191,7 +191,7 @@
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS"> <navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS">
<barButtonItem key="rightBarButtonItem" style="done" id="FLf-DS-F77"> <barButtonItem key="rightBarButtonItem" style="plain" id="FLf-DS-F77">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="SideStore" customModuleProvider="target"> <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="287" y="6.5" width="72" height="31"/> <rect key="frame" x="287" y="6.5" width="72" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
@@ -229,7 +229,7 @@
<!--App--> <!--App-->
<scene sceneID="CgX-7h-sRI"> <scene sceneID="CgX-7h-sRI">
<objects> <objects>
<tableViewController storyboardIdentifier="appContentViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <tableViewController id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -511,7 +511,7 @@ World</string>
<!--Permission Popover View Controller--> <!--Permission Popover View Controller-->
<scene sceneID="24j-EJ-G4e"> <scene sceneID="24j-EJ-G4e">
<objects> <objects>
<viewController storyboardIdentifier="permissionPopoverViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX"> <view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX">
<rect key="frame" x="0.0" y="0.0" width="375" height="217"/> <rect key="frame" x="0.0" y="0.0" width="375" height="217"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -566,7 +566,7 @@ World</string>
<!--News--> <!--News-->
<scene sceneID="bqw-wB-hyB"> <scene sceneID="bqw-wB-hyB">
<objects> <objects>
<collectionViewController storyboardIdentifier="newsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -592,7 +592,7 @@ World</string>
<!--Browse--> <!--Browse-->
<scene sceneID="VHa-uP-bFU"> <scene sceneID="VHa-uP-bFU">
<objects> <objects>
<navigationController storyboardIdentifier="forwardingNavigationControllerBrowse" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/> <tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
@@ -620,7 +620,7 @@ World</string>
<!--My Apps--> <!--My Apps-->
<scene sceneID="nhh-BJ-XiT"> <scene sceneID="nhh-BJ-XiT">
<objects> <objects>
<navigationController storyboardIdentifier="forwardingNavigationControllerNyApps" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y"> <tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
<color key="badgeColor" name="Primary"/> <color key="badgeColor" name="Primary"/>
</tabBarItem> </tabBarItem>
@@ -641,7 +641,7 @@ World</string>
<!--My Apps--> <!--My Apps-->
<scene sceneID="EC8-Sf-AF9"> <scene sceneID="EC8-Sf-AF9">
<objects> <objects>
<collectionViewController storyboardIdentifier="myAppsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -660,7 +660,7 @@ World</string>
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/> <rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="60"/> <rect key="frame" x="8" y="0.0" width="359" height="60"/>
</view> </view>
</subviews> </subviews>
@@ -797,7 +797,7 @@ World</string>
<!--App IDs--> <!--App IDs-->
<scene sceneID="kvf-US-rRe"> <scene sceneID="kvf-US-rRe">
<objects> <objects>
<collectionViewController storyboardIdentifier="appIDsViewController" title="App IDs" useStoryboardIdentifierAsRestorationIdentifier="YES" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController title="App IDs" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -816,7 +816,7 @@ World</string>
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/> <rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="80"/> <rect key="frame" x="8" y="0.0" width="359" height="80"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/> <bool key="isElement" value="YES"/>
@@ -835,7 +835,7 @@ World</string>
</connections> </connections>
</collectionViewCell> </collectionViewCell>
</cells> </cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStoreAppKit"> <collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/> <rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
@@ -856,7 +856,7 @@ World</string>
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/> <outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
</connections> </connections>
</collectionReusableView> </collectionReusableView>
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStoreAppKit"> <collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="170" width="375" height="50"/> <rect key="frame" x="0.0" y="170" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
@@ -881,9 +881,9 @@ World</string>
</connections> </connections>
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb"> <navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
<barButtonItem key="leftBarButtonItem" style="done" id="Aqs-QK-Ups"> <barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba"> <view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
<rect key="frame" x="16" y="7" width="83" height="42"/> <rect key="frame" x="16" y="1" width="83" height="42"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view> </view>
</barButtonItem> </barButtonItem>
@@ -905,7 +905,7 @@ World</string>
<!--News--> <!--News-->
<scene sceneID="BV8-6J-nIv"> <scene sceneID="BV8-6J-nIv">
<objects> <objects>
<navigationController storyboardIdentifier="forwardingNavigationControllerNews" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/> <tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
@@ -925,10 +925,10 @@ World</string>
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="1Gj-mS-BaN"> <scene sceneID="1Gj-mS-BaN">
<objects> <objects>
<navigationController storyboardIdentifier="nav1" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="IXk-qg-mFJ" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
@@ -943,7 +943,7 @@ World</string>
<!--Sources--> <!--Sources-->
<scene sceneID="0S1-zn-9KZ"> <scene sceneID="0S1-zn-9KZ">
<objects> <objects>
<collectionViewController storyboardIdentifier="sourcesViewController" title="Sources" useStoryboardIdentifierAsRestorationIdentifier="YES" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController title="Sources" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -962,7 +962,7 @@ World</string>
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/> <rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="80"/> <rect key="frame" x="8" y="0.0" width="359" height="80"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/> <bool key="isElement" value="YES"/>
@@ -981,7 +981,7 @@ World</string>
</connections> </connections>
</collectionViewCell> </collectionViewCell>
</cells> </cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStoreAppKit"> <collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/> <rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
@@ -1067,10 +1067,10 @@ World</string>
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="6NV-LQ-gKB"> <scene sceneID="6NV-LQ-gKB">
<objects> <objects>
<navigationController storyboardIdentifier="nav2" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Qo4-72-Hmr" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
@@ -1095,13 +1095,13 @@ World</string>
<image name="News" width="19" height="20"/> <image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/> <image name="Settings" width="20" height="20"/>
<namedColor name="Background"> <namedColor name="Background">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="BlurTint"> <namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/> <color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="Primary"> <namedColor name="Primary">
<color red="0.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> </namedColor>
<systemColor name="systemBackgroundColor"> <systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@@ -8,21 +8,17 @@
import UIKit import UIKit
import RoxasUIKit import Roxas
import OSLog
#if canImport(Logging)
import Logging
#endif
import Nuke import Nuke
@objc final class BrowseCollectionViewCell: UICollectionViewCell { @objc final class BrowseCollectionViewCell: UICollectionViewCell
{
var imageURLs: [URL] = [] { var imageURLs: [URL] = [] {
didSet { didSet {
dataSource.items = imageURLs as [NSURL] self.dataSource.items = self.imageURLs as [NSURL]
} }
} }
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
@IBOutlet var bannerView: AppBannerView! @IBOutlet var bannerView: AppBannerView!
@@ -30,49 +26,56 @@ import Nuke
@IBOutlet private(set) var screenshotsCollectionView: UICollectionView! @IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
contentView.preservesSuperviewLayoutMargins = true self.contentView.preservesSuperviewLayoutMargins = true
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷. // Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷.
screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
screenshotsCollectionView.delegate = self self.screenshotsCollectionView.delegate = self
screenshotsCollectionView.dataSource = dataSource self.screenshotsCollectionView.dataSource = self.dataSource
screenshotsCollectionView.prefetchDataSource = dataSource self.screenshotsCollectionView.prefetchDataSource = self.dataSource
} }
} }
private extension BrowseCollectionViewCell { private extension BrowseCollectionViewCell
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> { {
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
{
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: []) let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: [])
dataSource.cellConfigurationHandler = { cell, _, _ in dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.image = nil cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true cell.imageView.isIndicatingActivity = true
} }
dataSource.prefetchHandler = { imageURL, _, completionHandler in dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
RSTAsyncBlockOperation { operation in return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL as URL, processor: .screenshot) let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() } guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image { if let image = response?.image
{
completionHandler(image, nil) completionHandler(image, nil)
} else { }
else
{
completionHandler(nil, error) completionHandler(nil, error)
} }
}) })
} }
} }
dataSource.prefetchCompletionHandler = { cell, image, _, error in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false cell.imageView.isIndicatingActivity = false
cell.imageView.image = image cell.imageView.image = image
if let error = error { if let error = error
os_log("Error loading image: %@", type: .error , error.localizedDescription) {
print("Error loading image:", error)
} }
} }
@@ -80,8 +83,10 @@ private extension BrowseCollectionViewCell {
} }
} }
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout { extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt _: IndexPath) -> CGSize { {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
// Assuming 9.0 / 16.0 ratio for now. // Assuming 9.0 / 16.0 ratio for now.
let aspectRatio: CGFloat = 9.0 / 16.0 let aspectRatio: CGFloat = 9.0 / 16.0

View File

@@ -1,28 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="browseCollectionViewCell" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="SideStoreAppKit"> <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/> <rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/> <rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
<rect key="frame" x="16" y="0.0" width="343" height="369"/> <rect key="frame" x="16" y="0.0" width="343" height="369"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/> <rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/> <constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
</constraints> </constraints>
@@ -62,9 +61,4 @@
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/> <point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
</collectionViewCell> </collectionViewCell>
</objects> </objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document> </document>

View File

@@ -8,16 +8,13 @@
import UIKit import UIKit
import SideStoreCore import AltStoreCore
import RoxasUIKit import Roxas
import OSLog
#if canImport(Logging)
import Logging
#endif
import Nuke import Nuke
class BrowseViewController: UICollectionViewController { class BrowseViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
private lazy var placeholderView = RSTPlaceholderView(frame: .zero) private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
@@ -25,7 +22,7 @@ class BrowseViewController: UICollectionViewController {
private var loadingState: LoadingState = .loading { private var loadingState: LoadingState = .loading {
didSet { didSet {
update() self.update()
} }
} }
@@ -33,42 +30,47 @@ class BrowseViewController: UICollectionViewController {
@IBOutlet private var sourcesBarButtonItem: UIBarButtonItem! @IBOutlet private var sourcesBarButtonItem: UIBarButtonItem!
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
#if BETA #if BETA
dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)] self.dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
navigationItem.searchController = dataSource.searchController self.navigationItem.searchController = self.dataSource.searchController
#endif #endif
prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) self.collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
collectionView.dataSource = dataSource self.collectionView.dataSource = self.dataSource
collectionView.prefetchDataSource = dataSource self.collectionView.prefetchDataSource = self.dataSource
registerForPreviewing(with: self, sourceView: collectionView) self.registerForPreviewing(with: self, sourceView: self.collectionView)
update() self.update()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated) super.viewWillAppear(animated)
fetchSource() self.fetchSource()
updateDataSource() self.updateDataSource()
update() self.update()
} }
@IBAction private func unwindFromSourcesViewController(_: UIStoryboardSegue) { @IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
fetchSource() {
self.fetchSource()
} }
} }
private extension BrowseViewController { private extension BrowseViewController
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage> { {
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
{
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp> let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true), fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true), NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
@@ -78,7 +80,7 @@ private extension BrowseViewController {
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID) fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellConfigurationHandler = { cell, app, _ in dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
let cell = cell as! BrowseCollectionViewCell let cell = cell as! BrowseCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right cell.layoutMargins.right = self.view.layoutMargins.right
@@ -101,7 +103,8 @@ private extension BrowseViewController {
let tintColor = app.tintColor ?? .altPrimary let tintColor = app.tintColor ?? .altPrimary
cell.tintColor = tintColor cell.tintColor = tintColor
if app.installedApp == nil { if app.installedApp == nil
{
let buttonTitle = NSLocalizedString("Free", comment: "") let buttonTitle = NSLocalizedString("Free", comment: "")
cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal) cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name)
@@ -110,12 +113,17 @@ private extension BrowseViewController {
let progress = AppManager.shared.installationProgress(for: app) let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress cell.bannerView.button.progress = progress
if let versionDate = app.latestVersion?.date, versionDate > Date() { if let versionDate = app.latestVersion?.date, versionDate > Date()
{
cell.bannerView.button.countdownDate = app.versionDate cell.bannerView.button.countdownDate = app.versionDate
} else { }
else
{
cell.bannerView.button.countdownDate = nil cell.bannerView.button.countdownDate = nil
} }
} else { }
else
{
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal) cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name)
cell.bannerView.button.accessibilityValue = nil cell.bannerView.button.accessibilityValue = nil
@@ -123,59 +131,72 @@ private extension BrowseViewController {
cell.bannerView.button.countdownDate = nil cell.bannerView.button.countdownDate = nil
} }
} }
dataSource.prefetchHandler = { storeApp, _, completionHandler -> Foundation.Operation? in dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
let iconURL = storeApp.iconURL let iconURL = storeApp.iconURL
return RSTAsyncBlockOperation { operation in return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { response, error in ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() } guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image { if let image = response?.image
{
completionHandler(image, nil) completionHandler(image, nil)
} else { }
else
{
completionHandler(nil, error) completionHandler(nil, error)
} }
}) })
} }
} }
dataSource.prefetchCompletionHandler = { cell, image, _, error in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! BrowseCollectionViewCell let cell = cell as! BrowseCollectionViewCell
cell.bannerView.iconImageView.isIndicatingActivity = false cell.bannerView.iconImageView.isIndicatingActivity = false
cell.bannerView.iconImageView.image = image cell.bannerView.iconImageView.image = image
if let error = error { if let error = error
os_log("Error loading image: %@", type: .error , error.localizedDescription) {
print("Error loading image:", error)
} }
} }
dataSource.placeholderView = placeholderView dataSource.placeholderView = self.placeholderView
return dataSource return dataSource
} }
func updateDataSource() { func updateDataSource()
dataSource.predicate = nil {
self.dataSource.predicate = nil
} }
func fetchSource() { func fetchSource()
loadingState = .loading {
self.loadingState = .loading
AppManager.shared.fetchSources { result in AppManager.shared.fetchSources() { (result) in
do { do
do { {
do
{
let (_, context) = try result.get() let (_, context) = try result.get()
try context.save() try context.save()
DispatchQueue.main.async { DispatchQueue.main.async {
self.loadingState = .finished(.success(())) self.loadingState = .finished(.success(()))
} }
} catch let error as AppManager.FetchSourcesError { }
catch let error as AppManager.FetchSourcesError
{
try error.managedObjectContext?.save() try error.managedObjectContext?.save()
throw error throw error
} }
} catch { }
catch
{
DispatchQueue.main.async { DispatchQueue.main.async {
if self.dataSource.itemCount > 0 { if self.dataSource.itemCount > 0
{
let toastView = ToastView(error: error) let toastView = ToastView(error: error)
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside) toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self) toastView.show(in: self)
@@ -187,140 +208,157 @@ private extension BrowseViewController {
} }
} }
func update() { func update()
switch loadingState { {
switch self.loadingState
{
case .loading: case .loading:
placeholderView.textLabel.isHidden = true self.placeholderView.textLabel.isHidden = true
placeholderView.detailTextLabel.isHidden = false self.placeholderView.detailTextLabel.isHidden = false
placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "") self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
placeholderView.activityIndicatorView.startAnimating() self.placeholderView.activityIndicatorView.startAnimating()
case let .finished(.failure(error)): case .finished(.failure(let error)):
placeholderView.textLabel.isHidden = false self.placeholderView.textLabel.isHidden = false
placeholderView.detailTextLabel.isHidden = false self.placeholderView.detailTextLabel.isHidden = false
placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "") self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "")
placeholderView.detailTextLabel.text = error.localizedDescription self.placeholderView.detailTextLabel.text = error.localizedDescription
placeholderView.activityIndicatorView.stopAnimating() self.placeholderView.activityIndicatorView.stopAnimating()
case .finished(.success): case .finished(.success):
placeholderView.textLabel.isHidden = true self.placeholderView.textLabel.isHidden = true
placeholderView.detailTextLabel.isHidden = true self.placeholderView.detailTextLabel.isHidden = true
placeholderView.activityIndicatorView.stopAnimating() self.placeholderView.activityIndicatorView.stopAnimating()
} }
} }
} }
private extension BrowseViewController { private extension BrowseViewController
@IBAction func performAppAction(_ sender: PillButton) { {
let point = collectionView.convert(sender.center, from: sender.superview) @IBAction func performAppAction(_ sender: PillButton)
guard let indexPath = collectionView.indexPathForItem(at: point) else { return } {
let point = self.collectionView.convert(sender.center, from: sender.superview)
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
let app = dataSource.item(at: indexPath) let app = self.dataSource.item(at: indexPath)
if let installedApp = app.installedApp { if let installedApp = app.installedApp
open(installedApp) {
} else { self.open(installedApp)
install(app, at: indexPath) }
else
{
self.install(app, at: indexPath)
} }
} }
func install(_ app: StoreApp, at indexPath: IndexPath) { func install(_ app: StoreApp, at indexPath: IndexPath)
{
let previousProgress = AppManager.shared.installationProgress(for: app) let previousProgress = AppManager.shared.installationProgress(for: app)
guard previousProgress == nil else { guard previousProgress == nil else {
previousProgress?.cancel() previousProgress?.cancel()
return return
} }
_ = AppManager.shared.install(app, presentingViewController: self) { result in _ = AppManager.shared.install(app, presentingViewController: self) { (result) in
DispatchQueue.main.async { DispatchQueue.main.async {
switch result { switch result
{
case .failure(OperationError.cancelled): break // Ignore case .failure(OperationError.cancelled): break // Ignore
case let .failure(error): case .failure(let error):
let toastView = ToastView(error: error) let toastView = ToastView(error: error)
toastView.show(in: self) toastView.show(in: self)
case .success: os_log("Installed app: %@", type: .info , app.bundleIdentifier) case .success: print("Installed app:", app.bundleIdentifier)
} }
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
} }
} }
collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
} }
func open(_ installedApp: InstalledApp) { func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL) UIApplication.shared.open(installedApp.openAppURL)
} }
} }
extension BrowseViewController: UICollectionViewDelegateFlowLayout { extension BrowseViewController: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { {
let item = dataSource.item(at: indexPath) func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let item = self.dataSource.item(at: indexPath)
if let previousSize = cachedItemSizes[item.bundleIdentifier] { if let previousSize = self.cachedItemSizes[item.bundleIdentifier]
{
return previousSize return previousSize
} }
let maxVisibleScreenshots = 2 as CGFloat let maxVisibleScreenshots = 2 as CGFloat
let aspectRatio: CGFloat = 16.0 / 9.0 let aspectRatio: CGFloat = 16.0 / 9.0
let layout = prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout let layout = self.prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
dataSource.cellConfigurationHandler(prototypeCell, item, indexPath) self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
let widthConstraint = prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width) let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
widthConstraint.isActive = true widthConstraint.isActive = true
defer { widthConstraint.isActive = false } defer { widthConstraint.isActive = false }
// Manually update cell width & layout so we can accurately calculate screenshot sizes. // Manually update cell width & layout so we can accurately calculate screenshot sizes.
prototypeCell.frame.size.width = widthConstraint.constant self.prototypeCell.frame.size.width = widthConstraint.constant
prototypeCell.layoutIfNeeded() self.prototypeCell.layoutIfNeeded()
let collectionViewWidth = prototypeCell.screenshotsCollectionView.bounds.width let collectionViewWidth = self.prototypeCell.screenshotsCollectionView.bounds.width
let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down) let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down)
let screenshotHeight = screenshotWidth * aspectRatio let screenshotHeight = screenshotWidth * aspectRatio
let heightConstraint = prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight) let heightConstraint = self.prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error. heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
heightConstraint.isActive = true heightConstraint.isActive = true
defer { heightConstraint.isActive = false } defer { heightConstraint.isActive = false }
let itemSize = prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) let itemSize = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
cachedItemSizes[item.bundleIdentifier] = itemSize self.cachedItemSizes[item.bundleIdentifier] = itemSize
return itemSize return itemSize
} }
override func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
let app = dataSource.item(at: indexPath) {
let app = self.dataSource.item(at: indexPath)
let appViewController = AppViewController.makeAppViewController(app: app) let appViewController = AppViewController.makeAppViewController(app: app)
navigationController?.pushViewController(appViewController, animated: true) self.navigationController?.pushViewController(appViewController, animated: true)
} }
} }
extension BrowseViewController: UIViewControllerPreviewingDelegate { extension BrowseViewController: UIViewControllerPreviewingDelegate
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { {
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
{
guard guard
let indexPath = collectionView.indexPathForItem(at: location), let indexPath = self.collectionView.indexPathForItem(at: location),
let cell = collectionView.cellForItem(at: indexPath) let cell = self.collectionView.cellForItem(at: indexPath)
else { return nil } else { return nil }
previewingContext.sourceRect = cell.frame previewingContext.sourceRect = cell.frame
let app = dataSource.item(at: indexPath) let app = self.dataSource.item(at: indexPath)
let appViewController = AppViewController.makeAppViewController(app: app) let appViewController = AppViewController.makeAppViewController(app: app)
return appViewController return appViewController
} }
func previewingContext(_: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
navigationController?.pushViewController(viewControllerToCommit, animated: true) {
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
} }
} }

View 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
}
}

View 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
}
}

View File

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="SideStoreAppKit"> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<connections> <connections>
<outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/> <outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/>
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/> <outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
@@ -23,7 +23,7 @@
</connections> </connections>
</placeholder> </placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="appBannerView" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5"> <view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/> <rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
@@ -46,7 +46,7 @@
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info"> <stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/> <rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="SideStore" customModuleProvider="target"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="14" y="14" width="60" height="60"/> <rect key="frame" x="14" y="14" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/> <constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
@@ -110,7 +110,7 @@
<color key="textColor" systemColor="secondaryLabelColor"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="SideStore" customModuleProvider="target"> <button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/> <rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>

View 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
}
}

View File

@@ -8,8 +8,9 @@
import AVFoundation import AVFoundation
public final class BackgroundTaskManager { final class BackgroundTaskManager
public static let shared = BackgroundTaskManager() {
static let shared = BackgroundTaskManager()
private var isPlaying = false private var isPlaying = false
@@ -19,37 +20,45 @@ public final class BackgroundTaskManager {
private let audioEngineQueue: DispatchQueue private let audioEngineQueue: DispatchQueue
private init() { private init()
audioEngine = AVAudioEngine() {
audioEngine.mainMixerNode.outputVolume = 0.0 self.audioEngine = AVAudioEngine()
self.audioEngine.mainMixerNode.outputVolume = 0.0
player = AVAudioPlayerNode() self.player = AVAudioPlayerNode()
audioEngine.attach(player) self.audioEngine.attach(self.player)
do { do
{
let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")! let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")!
audioFile = try AVAudioFile(forReading: audioFileURL) self.audioFile = try AVAudioFile(forReading: audioFileURL)
audioEngine.connect(player, to: audioEngine.mainMixerNode, format: audioFile.processingFormat) self.audioEngine.connect(self.player, to: self.audioEngine.mainMixerNode, format: self.audioFile.processingFormat)
} catch { }
catch
{
fatalError("Error. \(error)") fatalError("Error. \(error)")
} }
audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager") self.audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager")
} }
} }
public extension BackgroundTaskManager { extension BackgroundTaskManager
func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void)) { {
func finish() { func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void))
player.stop() {
audioEngine.stop() func finish()
{
self.player.stop()
self.audioEngine.stop()
isPlaying = false self.isPlaying = false
} }
audioEngineQueue.sync { self.audioEngineQueue.sync {
do { do
{
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers) try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true) try AVAudioSession.sharedInstance().setActive(true)
@@ -68,7 +77,9 @@ public extension BackgroundTaskManager {
taskHandler(.success(())) { taskHandler(.success(())) {
finish() finish()
} }
} catch { }
catch
{
taskHandler(.failure(error)) { taskHandler(.failure(error)) {
finish() finish()
} }
@@ -77,9 +88,11 @@ public extension BackgroundTaskManager {
} }
} }
private extension BackgroundTaskManager { private extension BackgroundTaskManager
func scheduleAudioFile() { {
player.scheduleFile(audioFile, at: nil) { func scheduleAudioFile()
{
self.player.scheduleFile(self.audioFile, at: nil) {
self.audioEngineQueue.async { self.audioEngineQueue.async {
guard self.isPlaying else { return } guard self.isPlaying else { return }
self.scheduleAudioFile() self.scheduleAudioFile()

View File

@@ -8,18 +8,20 @@
import UIKit import UIKit
@objc final class BannerCollectionViewCell: UICollectionViewCell
final class BannerCollectionViewCell: UICollectionViewCell { {
private(set) var errorBadge: UIView? private(set) var errorBadge: UIView?
@IBOutlet private(set) var bannerView: AppBannerView! @IBOutlet private(set) var bannerView: AppBannerView!
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.preservesSuperviewLayoutMargins = true self.contentView.preservesSuperviewLayoutMargins = true
if #available(iOS 13.0, *) { if #available(iOS 13.0, *)
{
let errorBadge = UIView() let errorBadge = UIView()
errorBadge.translatesAutoresizingMaskIntoConstraints = false errorBadge.translatesAutoresizingMaskIntoConstraints = false
errorBadge.isHidden = true errorBadge.isHidden = true

View File

@@ -8,7 +8,8 @@
import UIKit import UIKit
final class Button: UIButton { final class Button: UIButton
{
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize var size = super.intrinsicContentSize
size.width += 20 size.width += 20
@@ -16,21 +17,23 @@ final class Button: UIButton {
return size return size
} }
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
setTitleColor(.white, for: .normal) self.setTitleColor(.white, for: .normal)
layer.masksToBounds = true self.layer.masksToBounds = true
layer.cornerRadius = 8 self.layer.cornerRadius = 8
update() self.update()
} }
override func tintColorDidChange() { override func tintColorDidChange()
{
super.tintColorDidChange() super.tintColorDidChange()
update() self.update()
} }
override var isHighlighted: Bool { override var isHighlighted: Bool {
@@ -41,17 +44,22 @@ final class Button: UIButton {
override var isEnabled: Bool { override var isEnabled: Bool {
didSet { didSet {
update() self.update()
} }
} }
} }
private extension Button { private extension Button
func update() { {
if isEnabled { func update()
backgroundColor = tintColor {
} else { if self.isEnabled
backgroundColor = .lightGray {
self.backgroundColor = self.tintColor
}
else
{
self.backgroundColor = .lightGray
} }
} }
} }

View 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
}
}

View File

@@ -8,12 +8,13 @@
import UIKit import UIKit
final class ForwardingNavigationController: UINavigationController { final class ForwardingNavigationController: UINavigationController
{
override var childForStatusBarStyle: UIViewController? { override var childForStatusBarStyle: UIViewController? {
self.topViewController return self.topViewController
} }
override var childForStatusBarHidden: UIViewController? { override var childForStatusBarHidden: UIViewController? {
topViewController return self.topViewController
} }
} }

View File

@@ -8,29 +8,32 @@
import UIKit import UIKit
import RoxasUIKit import Roxas
@objc final class NavigationBar: UINavigationBar
final class NavigationBar: UINavigationBar { {
@objc
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true @IBInspectable var automaticallyAdjustsItemPositions: Bool = true
private let backgroundColorView = UIView() private let backgroundColorView = UIView()
override init(frame: CGRect) { override init(frame: CGRect)
{
super.init(frame: frame) super.init(frame: frame)
initialize() self.initialize()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder) super.init(coder: aDecoder)
initialize() self.initialize()
} }
private func initialize() { private func initialize()
if #available(iOS 13, *) { {
if #available(iOS 13, *)
{
let standardAppearance = UINavigationBarAppearance() let standardAppearance = UINavigationBarAppearance()
standardAppearance.configureWithDefaultBackground() standardAppearance.configureWithDefaultBackground()
standardAppearance.shadowColor = nil standardAppearance.shadowColor = nil
@@ -40,7 +43,8 @@ final class NavigationBar: UINavigationBar {
edgeAppearance.backgroundColor = self.barTintColor edgeAppearance.backgroundColor = self.barTintColor
edgeAppearance.shadowColor = nil edgeAppearance.shadowColor = nil
if let tintColor = self.barTintColor { if let tintColor = self.barTintColor
{
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white] let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
standardAppearance.backgroundColor = tintColor standardAppearance.backgroundColor = tintColor
@@ -49,37 +53,48 @@ final class NavigationBar: UINavigationBar {
edgeAppearance.titleTextAttributes = textAttributes edgeAppearance.titleTextAttributes = textAttributes
edgeAppearance.largeTitleTextAttributes = textAttributes edgeAppearance.largeTitleTextAttributes = textAttributes
} else { }
else
{
standardAppearance.backgroundColor = nil standardAppearance.backgroundColor = nil
} }
self.scrollEdgeAppearance = edgeAppearance self.scrollEdgeAppearance = edgeAppearance
self.standardAppearance = standardAppearance self.standardAppearance = standardAppearance
} else { }
shadowImage = UIImage() else
{
self.shadowImage = UIImage()
if let tintColor = barTintColor { if let tintColor = self.barTintColor
backgroundColorView.backgroundColor = tintColor {
self.backgroundColorView.backgroundColor = tintColor
// Top = -50 to cover status bar area above navigation bar on any device. // Top = -50 to cover status bar area above navigation bar on any device.
// Bottom = -1 to prevent a flickering gray line from appearing. // Bottom = -1 to prevent a flickering gray line from appearing.
addSubview(backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0)) self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
} else { }
barTintColor = .white else
{
self.barTintColor = .white
} }
} }
} }
override func layoutSubviews() { override func layoutSubviews()
{
super.layoutSubviews() super.layoutSubviews()
if backgroundColorView.superview != nil { if self.backgroundColorView.superview != nil
insertSubview(backgroundColorView, at: 1) {
self.insertSubview(self.backgroundColorView, at: 1)
} }
if automaticallyAdjustsItemPositions { if self.automaticallyAdjustsItemPositions
{
// We can't easily shift just the back button up, so we shift the entire content view slightly. // We can't easily shift just the back button up, so we shift the entire content view slightly.
for contentView in subviews { for contentView in self.subviews
{
guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue } guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue }
contentView.center.y -= 2 contentView.center.y -= 2
} }

View 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
}
}
}

View File

@@ -8,8 +8,8 @@
import UIKit import UIKit
@objc class TextCollectionReusableView: UICollectionReusableView
public class TextCollectionReusableView: UICollectionReusableView { {
@IBOutlet var textLabel: UILabel! @IBOutlet var textLabel: UILabel!
@IBOutlet var topLayoutConstraint: NSLayoutConstraint! @IBOutlet var topLayoutConstraint: NSLayoutConstraint!

View File

@@ -6,42 +6,48 @@
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import RoxasUIKit import Roxas
import SideStoreCore import AltStoreCore
import SideKit
import AltSign
extension TimeInterval { extension TimeInterval
{
static let shortToastViewDuration = 4.0 static let shortToastViewDuration = 4.0
static let longToastViewDuration = 8.0 static let longToastViewDuration = 8.0
} }
final class ToastView: RSTToastView { final class ToastView: RSTToastView
{
var preferredDuration: TimeInterval var preferredDuration: TimeInterval
override init(text: String, detailText detailedText: String?) { override init(text: String, detailText detailedText: String?)
if detailedText == nil { {
preferredDuration = .shortToastViewDuration if detailedText == nil
} else { {
preferredDuration = .longToastViewDuration self.preferredDuration = .shortToastViewDuration
}
else
{
self.preferredDuration = .longToastViewDuration
} }
super.init(text: text, detailText: detailedText) super.init(text: text, detailText: detailedText)
isAccessibilityElement = true self.isAccessibilityElement = true
layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16) self.layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16)
setNeedsLayout() self.setNeedsLayout()
if let stackView = textLabel.superview as? UIStackView { if let stackView = self.textLabel.superview as? UIStackView
{
// RSTToastView does not expose stack view containing labels, // RSTToastView does not expose stack view containing labels,
// so we access it indirectly as the labels' superview. // so we access it indirectly as the labels' superview.
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0 stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
} }
} }
convenience init(error: Error) { convenience init(error: Error)
{
var error = error as NSError var error = error as NSError
var underlyingError = error.underlyingError var underlyingError = error.underlyingError
@@ -49,7 +55,7 @@ final class ToastView: RSTToastView {
if if
let unwrappedUnderlyingError = underlyingError, 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. // Treat underlyingError as the primary error.
@@ -62,46 +68,54 @@ final class ToastView: RSTToastView {
let text: String let text: String
let detailText: String? let detailText: String?
if let failure = error.localizedFailure { if let failure = error.localizedFailure
{
text = failure text = failure
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
} else if let reason = error.localizedFailureReason { }
else if let reason = error.localizedFailureReason
{
text = reason text = reason
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
} else { }
else
{
text = error.localizedDescription text = error.localizedDescription
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
} }
self.init(text: text, detailText: detailText) self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration { if let preferredDuration = preferredDuration
{
self.preferredDuration = preferredDuration self.preferredDuration = preferredDuration
} }
} }
@available(*, unavailable) required init(coder aDecoder: NSCoder) {
required init(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func layoutSubviews() { override func layoutSubviews()
{
super.layoutSubviews() super.layoutSubviews()
// Rough calculation to determine height of ToastView with one-line textLabel. // Rough calculation to determine height of ToastView with one-line textLabel.
let minimumHeight = textLabel.font.lineHeight.rounded() + 18 let minimumHeight = self.textLabel.font.lineHeight.rounded() + 18
layer.cornerRadius = minimumHeight / 2 self.layer.cornerRadius = minimumHeight/2
} }
func show(in viewController: UIViewController) { func show(in viewController: UIViewController)
show(in: viewController.navigationController?.view ?? viewController.view, duration: preferredDuration) {
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) super.show(in: view, duration: duration)
let announcement = (textLabel.text ?? "") + ". " + (detailTextLabel.text ?? "") let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
accessibilityLabel = announcement self.accessibilityLabel = announcement
// Minimum 0.75 delay to prevent announcement being cut off by VoiceOver. // Minimum 0.75 delay to prevent announcement being cut off by VoiceOver.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
@@ -109,7 +123,8 @@ final class ToastView: RSTToastView {
} }
} }
override func show(in view: UIView) { override func show(in view: UIView)
show(in: view, duration: preferredDuration) {
self.show(in: view, duration: self.preferredDuration)
} }
} }

View 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)"
}
}

View File

@@ -8,4 +8,6 @@
import Foundation import Foundation
public enum Consts {} public enum Consts {
}

View File

@@ -7,25 +7,27 @@
// //
import Foundation import Foundation
import OSLog
#if canImport(Logging)
import Logging
#endif
extension FileManager { extension FileManager
func directorySize(at directoryURL: URL) -> Int? { {
func directorySize(at directoryURL: URL) -> Int?
{
guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil } guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
var total = 0 var total: Int = 0
for case let fileURL as URL in enumerator { for case let fileURL as URL in enumerator
do { {
do
{
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
guard let fileSize = resourceValues.fileSize else { continue } guard let fileSize = resourceValues.fileSize else { continue }
total += fileSize total += fileSize
} catch { }
os_log("Failed to read file size for item: %@. %@", type: .error, fileURL.absoluteString, error.localizedDescription) catch
{
print("Failed to read file size for item: \(fileURL).", error)
} }
} }

View File

@@ -10,8 +10,10 @@ import Intents
// Requires iOS 14 in-app intent handling. // Requires iOS 14 in-app intent handling.
@available(iOS 14, *) @available(iOS 14, *)
extension INInteraction { extension INInteraction
static func refreshAllApps() -> INInteraction { {
static func refreshAllApps() -> INInteraction
{
let refreshAllIntent = RefreshAllIntent() let refreshAllIntent = RefreshAllIntent()
refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String

View File

@@ -10,7 +10,8 @@ import Foundation
import OSLog import OSLog
public let customLog = OSLog(subsystem: "org.sidestore.sidestore", public let customLog = OSLog(subsystem: "org.sidestore.sidestore",
category: "ios") category: "ios")
public extension OSLog { public extension OSLog {
/// Error logger extension /// Error logger extension
@@ -48,7 +49,7 @@ public extension OSLog {
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable @inlinable
public func ELOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) { public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.error(message, args) OSLog.error(message, args)
} }
@@ -57,7 +58,7 @@ public func ELOG(_ message: StaticString, file _: StaticString = #file, function
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable @inlinable
public func ILOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) { public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.info(message, args) OSLog.info(message, args)
} }
@@ -66,8 +67,8 @@ public func ILOG(_ message: StaticString, file _: StaticString = #file, function
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable @inlinable
public func DLOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) { public func DLOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.debug(message, args) OSLog.debug(message, args)
} }
// MARK: Helpers // mark: Helpers

View File

@@ -6,10 +6,11 @@
// Copyright © 2020 Riley Testut. All rights reserved. // Copyright © 2020 Riley Testut. All rights reserved.
// //
import ARKit
import UIKit import UIKit
import ARKit
extension UIDevice { extension UIDevice
{
var isJailbroken: Bool { var isJailbroken: Bool {
if if
FileManager.default.fileExists(atPath: "/Applications/Cydia.app") || FileManager.default.fileExists(atPath: "/Applications/Cydia.app") ||
@@ -18,10 +19,12 @@ extension UIDevice {
FileManager.default.fileExists(atPath: "/usr/sbin/sshd") || FileManager.default.fileExists(atPath: "/usr/sbin/sshd") ||
FileManager.default.fileExists(atPath: "/etc/apt") || FileManager.default.fileExists(atPath: "/etc/apt") ||
FileManager.default.fileExists(atPath: "/private/var/lib/apt/") || FileManager.default.fileExists(atPath: "/private/var/lib/apt/") ||
UIApplication.shared.canOpenURL(URL(string: "cydia://")!) UIApplication.shared.canOpenURL(URL(string:"cydia://")!)
{ {
return true return true
} else { }
else
{
return false return false
} }
} }
@@ -29,11 +32,11 @@ extension UIDevice {
@available(iOS 14, *) @available(iOS 14, *)
var supportsFugu14: Bool { var supportsFugu14: Bool {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
return true return true
#else #else
// Fugu14 is supported on devices with an A12 processor or better. // 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. // ARKit 3 is only supported by devices with an A12 processor or better, according to the documentation.
return ARBodyTrackingConfiguration.isSupported return ARBodyTrackingConfiguration.isSupported
#endif #endif
} }

View File

@@ -8,32 +8,37 @@
import AudioToolbox import AudioToolbox
import CoreHaptics import CoreHaptics
import UIKit
private extension SystemSoundID { private extension SystemSoundID
{
static let pop = SystemSoundID(1520) static let pop = SystemSoundID(1520)
static let cancelled = SystemSoundID(1521) static let cancelled = SystemSoundID(1521)
static let tryAgain = SystemSoundID(1102) static let tryAgain = SystemSoundID(1102)
} }
@available(iOS 13, *) @available(iOS 13, *)
extension UIDevice { extension UIDevice
enum VibrationPattern { {
enum VibrationPattern
{
case success case success
case error case error
} }
} }
@available(iOS 13, *) @available(iOS 13, *)
extension UIDevice { extension UIDevice
{
var isVibrationSupported: Bool { var isVibrationSupported: Bool {
CHHapticEngine.capabilitiesForHardware().supportsHaptics return CHHapticEngine.capabilitiesForHardware().supportsHaptics
} }
func vibrate(pattern: VibrationPattern) { func vibrate(pattern: VibrationPattern)
guard isVibrationSupported else { return } {
guard self.isVibrationSupported else { return }
switch pattern { switch pattern
{
case .success: AudioServicesPlaySystemSound(.tryAgain) case .success: AudioServicesPlaySystemSound(.tryAgain)
case .error: AudioServicesPlaySystemSound(.cancelled) case .error: AudioServicesPlaySystemSound(.cancelled)
} }

View File

@@ -8,8 +8,9 @@
import UIKit import UIKit
extension UIScreen { extension UIScreen
{
var isExtraCompactHeight: Bool { var isExtraCompactHeight: Bool {
fixedCoordinateSpace.bounds.height < 600 return self.fixedCoordinateSpace.bounds.height < 600
} }
} }

View File

@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>ALTAppGroups</key> <key>ALTAppGroups</key>
<array> <array>
<string>group.$(APP_GROUP_IDENTIFIER)</string> <string>group.$(APP_GROUP_IDENTIFIER)</string>
@@ -11,10 +9,12 @@
</array> </array>
<key>ALTDeviceID</key> <key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string> <string>00008101-000129D63698001E</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTServerID</key> <key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string> <string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTAnisetteURL</key>
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
@@ -44,6 +44,8 @@
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -91,13 +93,6 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_altserver._tcp</string> <string>_altserver._tcp</string>
@@ -136,10 +131,13 @@
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
@@ -206,5 +204,7 @@
</dict> </dict>
</dict> </dict>
</array> </array>
<key>UIFileSharingEnabled</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -7,15 +7,12 @@
// //
import Foundation import Foundation
import SideStoreCore
import Intents import AltStoreCore
import OSLog
#if canImport(Logging)
import Logging
#endif
@available(iOS 14, *) @available(iOS 14, *)
public final class IntentHandler: NSObject, RefreshAllIntentHandling { final class IntentHandler: NSObject, RefreshAllIntentHandling
{
private let queue = DispatchQueue(label: "io.altstore.IntentHandler") private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]() private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]()
@@ -23,13 +20,15 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
private var operations = [RefreshAllIntent: BackgroundRefreshAppsOperation]() private var operations = [RefreshAllIntent: BackgroundRefreshAppsOperation]()
public func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) { func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
{
// Refreshing apps usually, but not always, completes within alotted time. // Refreshing apps usually, but not always, completes within alotted time.
// As a workaround, we'll start refreshing apps in confirm() so we can // As a workaround, we'll start refreshing apps in confirm() so we can
// take advantage of some extra time before starting handle() timeout timer. // take advantage of some extra time before starting handle() timeout timer.
completionHandlers[intent] = { response in self.completionHandlers[intent] = { (response) in
if response.code != .ready { if response.code != .ready
{
// Operation finished before confirmation "timeout". // Operation finished before confirmation "timeout".
// Cache response to return it when handle() is called. // Cache response to return it when handle() is called.
self.queuedResponses[intent] = response self.queuedResponses[intent] = response
@@ -40,36 +39,47 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
// Give ourselves 9 extra seconds before starting handle() timeout timer. // Give ourselves 9 extra seconds before starting handle() timeout timer.
// 10 seconds or longer results in timeout regardless. // 10 seconds or longer results in timeout regardless.
queue.asyncAfter(deadline: .now() + 9.0) { self.queue.asyncAfter(deadline: .now() + 9.0) {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
} }
if !DatabaseManager.shared.isStarted { if !DatabaseManager.shared.isStarted
DatabaseManager.shared.start { error in {
if let error = error { DatabaseManager.shared.start() { (error) in
if let error = error
{
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription)) self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription))
} else { }
else
{
self.refreshApps(intent: intent) self.refreshApps(intent: intent)
} }
} }
} else { }
refreshApps(intent: intent) else
{
self.refreshApps(intent: intent)
} }
} }
public func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) { func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
completionHandlers[intent] = { response in {
self.completionHandlers[intent] = { (response) in
// Ignore .ready response from confirm() timeout. // Ignore .ready response from confirm() timeout.
guard response.code != .ready else { return } guard response.code != .ready else { return }
completion(response) completion(response)
} }
if let response = queuedResponses[intent] { if let response = self.queuedResponses[intent]
queuedResponses[intent] = nil {
finish(intent, response: response) self.queuedResponses[intent] = nil
} else { self.finish(intent, response: response)
queue.asyncAfter(deadline: .now() + 7.0) { }
if let operation = self.operations[intent] { else
{
self.queue.asyncAfter(deadline: .now() + 7.0) {
if let operation = self.operations[intent]
{
// We took too long to finish and return the final result, // We took too long to finish and return the final result,
// so we'll now present a normal notification when finished. // so we'll now present a normal notification when finished.
operation.presentsFinishedNotification = true operation.presentsFinishedNotification = true
@@ -82,36 +92,48 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
} }
@available(iOS 14, *) @available(iOS 14, *)
private extension IntentHandler { private extension IntentHandler
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse) { {
queue.async { func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse)
if let completionHandler = self.completionHandlers[intent] { {
self.queue.async {
if let completionHandler = self.completionHandlers[intent]
{
self.completionHandlers[intent] = nil self.completionHandlers[intent] = nil
completionHandler(response) completionHandler(response)
} else if response.code != .ready && response.code != .inProgress { }
else if response.code != .ready && response.code != .inProgress
{
// Queue response in case refreshing finishes after confirm() but before handle(). // Queue response in case refreshing finishes after confirm() but before handle().
self.queuedResponses[intent] = response self.queuedResponses[intent] = response
} }
} }
} }
func refreshApps(intent: RefreshAllIntent) { func refreshApps(intent: RefreshAllIntent)
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in {
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchActiveApps(in: context) let installedApps = InstalledApp.fetchActiveApps(in: context)
let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { result in let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { (result) in
do { do
{
let results = try result.get() let results = try result.get()
for (_, result) in results { for (_, result) in results
{
guard case let .failure(error) = result else { continue } guard case let .failure(error) = result else { continue }
throw error throw error
} }
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} catch RefreshError.noInstalledApps { }
catch RefreshError.noInstalledApps
{
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) 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.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription))
} }

View File

@@ -6,23 +6,22 @@
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import UIKit
import Roxas
import EmotionalDamage import EmotionalDamage
import minimuxer import minimuxer
import MiniMuxer
import SideStoreAppKit
import RoxasUIKit
import UIKit
import SideStoreCore import AltStoreCore
import UniformTypeIdentifiers import UniformTypeIdentifiers
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate { final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
{
private var didFinishLaunching = false private var didFinishLaunching = false
private var destinationViewController: UIViewController! private var destinationViewController: UIViewController!
override var launchConditions: [RSTLaunchCondition] { 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) DatabaseManager.shared.start(completionHandler: completionHandler)
} }
@@ -30,14 +29,15 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
} }
override var childForStatusBarStyle: UIViewController? { override var childForStatusBarStyle: UIViewController? {
self.children.first return self.children.first
} }
override var childForStatusBarHidden: UIViewController? { override var childForStatusBarHidden: UIViewController? {
self.children.first return self.children.first
} }
override func viewDidLoad() { override func viewDidLoad()
{
defer { defer {
// Create destinationViewController now so view controllers can register for receiving Notifications. // Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
@@ -45,16 +45,16 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
super.viewDidLoad() super.viewDidLoad()
} }
override func viewDidAppear(_: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true) super.viewDidAppear(true)
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else { guard let pf = fetchPairingFile() else {
displayError("Device pairing file not found.") displayError("Device pairing file not found.")
return return
} }
start_minimuxer_threads(pf) start_minimuxer_threads(pf)
#endif #endif
} }
@@ -70,11 +70,10 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
fm.fileExists(atPath: appResourcePath.path), fm.fileExists(atPath: appResourcePath.path),
let data = fm.contents(atPath: appResourcePath.path), let data = fm.contents(atPath: appResourcePath.path),
let contents = String(data: data, encoding: .utf8), let contents = String(data: data, encoding: .utf8),
!contents.isEmpty !contents.isEmpty {
{
print("Loaded ALTPairingFile from \(appResourcePath.path)") print("Loaded ALTPairingFile from \(appResourcePath.path)")
return contents return contents
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here") { } else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
print("Loaded ALTPairingFile from Info.plist") print("Loaded ALTPairingFile from Info.plist")
return plistString return plistString
} else { } else {
@@ -83,7 +82,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert) let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert)
// Create OK button with action handler // Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { _ in let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
// Try to load it from a file picker // Try to load it from a file picker
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil) var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data)) 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.shouldShowFileExtensions = true
documentPickerController.delegate = self documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil) self.present(documentPickerController, animated: true, completion: nil)
}) })
// Add OK button to a dialog message //Add OK button to a dialog message
dialogMessage.addAction(ok) dialogMessage.addAction(ok)
// Present Alert to // Present Alert to
present(dialogMessage, animated: true, completion: nil) self.present(dialogMessage, animated: true, completion: nil)
return nil return nil
} }
@@ -110,7 +109,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert) let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
// Present alert to user // Present alert to user
present(dialogMessage, animated: true, completion: nil) self.present(dialogMessage, animated: true, completion: nil)
} }
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
@@ -138,28 +137,28 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
displayError("Unable to read pairing file") displayError("Unable to read pairing file")
} }
if isSecuredURL { if (isSecuredURL) {
url.stopAccessingSecurityScopedResource() url.stopAccessingSecurityScopedResource()
} }
controller.dismiss(animated: true, completion: nil) 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.") displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
} }
func start_minimuxer_threads(_ pairing_file: String) { func start_minimuxer_threads(_ pairing_file: String) {
set_usbmuxd_socket() set_usbmuxd_socket()
#if false // Retries #if false // Retries
var res = start_minimuxer(pairing_file: pairing_file) var res = start_minimuxer(pairing_file: pairing_file)
var attempts = 10 var attempts = 10
while attempts != 0, res != 0 { while (attempts != 0 && res != 0) {
print("start_minimuxer `res` != 0, retry #\(attempts)") print("start_minimuxer `res` != 0, retry #\(attempts)")
res = start_minimuxer(pairing_file: pairing_file) res = start_minimuxer(pairing_file: pairing_file)
attempts -= 1 attempts -= 1
} }
#else #else
let res = start_minimuxer(pairing_file: pairing_file) let res = start_minimuxer(pairing_file: pairing_file)
#endif #endif
if res != 0 { if res != 0 {
displayError("minimuxer failed to start. Incorrect arguments were passed.") displayError("minimuxer failed to start. Incorrect arguments were passed.")
@@ -168,34 +167,43 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
} }
} }
extension LaunchViewController { extension LaunchViewController
override func handleLaunchError(_ error: Error) { {
do { override func handleLaunchError(_ error: Error)
{
do
{
throw error 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 title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
let errorDescription: String let errorDescription: String
if #available(iOS 14.5, *) { if #available(iOS 14.5, *)
{
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription } let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
errorDescription = errorMessages.joined(separator: "\n\n") errorDescription = errorMessages.joined(separator: "\n\n")
} else { }
else
{
errorDescription = error.debugDescription errorDescription = error.debugDescription
} }
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert) 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.handleLaunchConditions()
})) }))
self.present(alertController, animated: true, completion: nil) self.present(alertController, animated: true, completion: nil)
} }
} }
override func finishLaunching() { override func finishLaunching()
{
super.finishLaunching() super.finishLaunching()
guard !didFinishLaunching else { return } guard !self.didFinishLaunching else { return }
AppManager.shared.update() AppManager.shared.update()
AppManager.shared.updatePatronsIfNeeded() AppManager.shared.updatePatronsIfNeeded()
@@ -203,16 +211,16 @@ extension LaunchViewController {
// Add view controller as child (rather than presenting modally) // Add view controller as child (rather than presenting modally)
// so tint adjustment + card presentations works correctly. // so tint adjustment + card presentations works correctly.
destinationViewController.view.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height) self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
destinationViewController.view.alpha = 0.0 self.destinationViewController.view.alpha = 0.0
addChild(destinationViewController) self.addChild(self.destinationViewController)
view.addSubview(destinationViewController.view, pinningEdgesWith: .zero) self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
destinationViewController.didMove(toParent: self) self.destinationViewController.didMove(toParent: self)
UIView.animate(withDuration: 0.2) { UIView.animate(withDuration: 0.2) {
self.destinationViewController.view.alpha = 1.0 self.destinationViewController.view.alpha = 1.0
} }
didFinishLaunching = true self.didFinishLaunching = true
} }
} }

View 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
}
}
}

View 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")
}
}

View File

@@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" 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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <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"> <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"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </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"/> <rect key="frame" x="274" y="23" width="81" height="32"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/> <fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/>
<state key="normal" title="Refresh All"/> <state key="normal" title="Refresh All"/>
</button> </button>
</subviews> </subviews>
<viewLayoutGuide key="safeArea" id="N3q-SZ-Vyv"/>
<constraints> <constraints>
<constraint firstItem="zhW-Re-WNf" firstAttribute="leading" secondItem="eyV-eW-aLi" secondAttribute="leading" constant="20" id="Fo0-fL-UpD"/> <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="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 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"/> <constraint firstItem="iMf-wr-wRV" firstAttribute="firstBaseline" secondItem="zhW-Re-WNf" secondAttribute="firstBaseline" id="iU7-F2-XDu"/>
</constraints> </constraints>
<viewLayoutGuide key="safeArea" id="N3q-SZ-Vyv"/>
<connections> <connections>
<outlet property="button" destination="iMf-wr-wRV" id="kWT-cc-BjS"/> <outlet property="button" destination="iMf-wr-wRV" id="kWT-cc-BjS"/>
<outlet property="textLabel" destination="zhW-Re-WNf" id="UOg-4X-rWx"/> <outlet property="textLabel" destination="zhW-Re-WNf" id="UOg-4X-rWx"/>

View File

@@ -6,22 +6,24 @@
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import RoxasUIKit
import UIKit import UIKit
import Roxas
@objc final class InstalledAppCollectionViewCell: UICollectionViewCell
final class InstalledAppCollectionViewCell: UICollectionViewCell { {
private(set) var deactivateBadge: UIView? private(set) var deactivateBadge: UIView?
@IBOutlet var bannerView: AppBannerView! @IBOutlet var bannerView: AppBannerView!
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.preservesSuperviewLayoutMargins = true self.contentView.preservesSuperviewLayoutMargins = true
if #available(iOS 13.0, *) { if #available(iOS 13.0, *)
{
let deactivateBadge = UIView() let deactivateBadge = UIView()
deactivateBadge.translatesAutoresizingMaskIntoConstraints = false deactivateBadge.translatesAutoresizingMaskIntoConstraints = false
deactivateBadge.isHidden = true deactivateBadge.isHidden = true
@@ -53,42 +55,43 @@ final class InstalledAppCollectionViewCell: UICollectionViewCell {
} }
} }
@objc final class InstalledAppsCollectionFooterView: UICollectionReusableView
final class InstalledAppsCollectionFooterView: UICollectionReusableView { {
@IBOutlet var textLabel: UILabel! @IBOutlet var textLabel: UILabel!
@IBOutlet var button: UIButton! @IBOutlet var button: UIButton!
} }
@objc final class NoUpdatesCollectionViewCell: UICollectionViewCell
final class NoUpdatesCollectionViewCell: UICollectionViewCell { {
@IBOutlet var blurView: UIVisualEffectView! @IBOutlet var blurView: UIVisualEffectView!
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
contentView.preservesSuperviewLayoutMargins = true self.contentView.preservesSuperviewLayoutMargins = true
} }
} }
@objc final class UpdatesCollectionHeaderView: UICollectionReusableView
final class UpdatesCollectionHeaderView: UICollectionReusableView { {
let button = PillButton(type: .system) let button = PillButton(type: .system)
override init(frame: CGRect) { override init(frame: CGRect)
{
super.init(frame: frame) super.init(frame: frame)
button.translatesAutoresizingMaskIntoConstraints = false self.button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle(">", for: .normal) self.button.setTitle(">", for: .normal)
addSubview(button) self.addSubview(self.button)
NSLayoutConstraint.activate([button.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), NSLayoutConstraint.activate([self.button.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
button.topAnchor.constraint(equalTo: topAnchor), self.button.topAnchor.constraint(equalTo: self.topAnchor),
button.widthAnchor.constraint(equalToConstant: 50), self.button.widthAnchor.constraint(equalToConstant: 50),
button.heightAnchor.constraint(equalToConstant: 26)]) self.button.heightAnchor.constraint(equalToConstant: 26)])
} }
@available(*, unavailable) required init?(coder aDecoder: NSCoder) {
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
} }

Some files were not shown because too many files have changed in this diff Show More