diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 4a064af7..fba5b450 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -387,6 +387,8 @@ D56915072AD5E91B00A2B747 /* Regex+Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */; }; D56915092AD5F3E800A2B747 /* AltTests+Sources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */; }; D569A5042AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D569A5032AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift */; }; + D56D21402B7D9942007641C5 /* AltAppIconsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56D213F2B7D9942007641C5 /* AltAppIconsViewController.swift */; }; + D56D21422B7D9C41007641C5 /* AltIcons.plist in Resources */ = {isa = PBXBuildFile; fileRef = D56D21412B7D9C41007641C5 /* AltIcons.plist */; }; D5708417292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; }; D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; }; D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */; }; @@ -1071,6 +1073,8 @@ D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Regex+Permissions.swift"; sourceTree = ""; }; D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AltTests+Sources.swift"; sourceTree = ""; }; D569A5032AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPermissionsViewController.swift; sourceTree = ""; }; + D56D213F2B7D9942007641C5 /* AltAppIconsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltAppIconsViewController.swift; sourceTree = ""; }; + D56D21412B7D9C41007641C5 /* AltIcons.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = AltIcons.plist; sourceTree = ""; }; D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = ""; }; D571ADCD2A02FA7400B24B63 /* SourceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceError.swift; sourceTree = ""; }; D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = ""; }; @@ -2048,6 +2052,7 @@ children = ( 0E764E162ADFF5740043DD4E /* AltBackup.ipa */, BFD247762284B9A700981D42 /* Assets.xcassets */, + D56D21412B7D9C41007641C5 /* AltIcons.plist */, BF770E6822BD57DD002A40FE /* Silence.m4a */, ); path = Resources; @@ -2096,6 +2101,7 @@ BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */, BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */, BFF0B695232242D3007A79E1 /* LicensesViewController.swift */, + D56D213F2B7D9942007641C5 /* AltAppIconsViewController.swift */, D589170128C7D93500E39C8B /* Error Log */, B3EE16B52925E27D00B3B1F5 /* AnisetteManager.swift */, ); @@ -2913,6 +2919,8 @@ BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */, D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */, BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */, + BF44EEF3246B3A17002A52F2 /* AltBackup.ipa in Resources */, + D56D21422B7D9C41007641C5 /* AltIcons.plist in Resources */, BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */, BFD247772284B9A700981D42 /* Assets.xcassets in Resources */, BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */, @@ -3375,6 +3383,7 @@ D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */, D54058B92A1D6269008CCC58 /* AppPermissionProtocol.swift in Sources */, BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */, + D56D21402B7D9942007641C5 /* AltAppIconsViewController.swift in Sources */, BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */, D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */, D569A5042AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift in Sources */, @@ -4221,6 +4230,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = AltStore/AltStore.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -4262,6 +4272,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = AltStore/AltStore.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; diff --git a/AltStore/Resources/AltIcons.plist b/AltStore/Resources/AltIcons.plist new file mode 100644 index 00000000..f3c0f291 --- /dev/null +++ b/AltStore/Resources/AltIcons.plist @@ -0,0 +1,45 @@ + + + + + modern + + + name + AltStore + imageName + AppIcon + + + name + AltBlue + imageName + BlueIcon + + + name + Evil AltStore + imageName + EvilIcon + + + inset + + + name + EU - Inset + imageName + EUInsetIcon + + + classic + + + name + Classic + imageName + ClassicIcon + + + + diff --git a/AltStore/Resources/Assets.xcassets/App Icons/BlueIcon.appiconset/AltBlue.png b/AltStore/Resources/Assets.xcassets/App Icons/BlueIcon.appiconset/AltBlue.png new file mode 100644 index 00000000..9cec2142 Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/App Icons/BlueIcon.appiconset/AltBlue.png differ diff --git a/AltStore/Resources/Assets.xcassets/App Icons/BlueIcon.appiconset/Contents.json b/AltStore/Resources/Assets.xcassets/App Icons/BlueIcon.appiconset/Contents.json new file mode 100644 index 00000000..4e1fdb92 --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/App Icons/BlueIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "AltBlue.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Contents.json b/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Contents.json new file mode 100644 index 00000000..3e70d9de --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Contents.json @@ -0,0 +1,101 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Group 23_120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Group 23_180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "Group 23.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Group 23.png b/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Group 23.png new file mode 100644 index 00000000..a88f8a08 Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Group 23.png differ diff --git a/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Group 23_120.png b/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Group 23_120.png new file mode 100644 index 00000000..3626fdb7 Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Group 23_120.png differ diff --git a/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Group 23_180.png b/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Group 23_180.png new file mode 100644 index 00000000..245d2785 Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/App Icons/ClassicIcon.appiconset/Group 23_180.png differ diff --git a/AltStore/Resources/Assets.xcassets/App Icons/Contents.json b/AltStore/Resources/Assets.xcassets/App Icons/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/App Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltStore/Resources/Assets.xcassets/App Icons/EUInsetIcon.appiconset/Contents.json b/AltStore/Resources/Assets.xcassets/App Icons/EUInsetIcon.appiconset/Contents.json new file mode 100644 index 00000000..5aa757cf --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/App Icons/EUInsetIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "EU Inset.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltStore/Resources/Assets.xcassets/App Icons/EUInsetIcon.appiconset/EU Inset.png b/AltStore/Resources/Assets.xcassets/App Icons/EUInsetIcon.appiconset/EU Inset.png new file mode 100644 index 00000000..fa19a5dc Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/App Icons/EUInsetIcon.appiconset/EU Inset.png differ diff --git a/AltStore/Resources/Assets.xcassets/App Icons/EvilIcon.appiconset/AltEvil.png b/AltStore/Resources/Assets.xcassets/App Icons/EvilIcon.appiconset/AltEvil.png new file mode 100644 index 00000000..ddbe87bf Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/App Icons/EvilIcon.appiconset/AltEvil.png differ diff --git a/AltStore/Resources/Assets.xcassets/App Icons/EvilIcon.appiconset/Contents.json b/AltStore/Resources/Assets.xcassets/App Icons/EvilIcon.appiconset/Contents.json new file mode 100644 index 00000000..6e08f659 --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/App Icons/EvilIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "AltEvil.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltStore/Settings/AltAppIconsViewController.swift b/AltStore/Settings/AltAppIconsViewController.swift new file mode 100644 index 00000000..462bd726 --- /dev/null +++ b/AltStore/Settings/AltAppIconsViewController.swift @@ -0,0 +1,208 @@ +// +// AltAppIconsViewController.swift +// AltStore +// +// Created by Riley Testut on 2/14/24. +// Copyright © 2024 Riley Testut. All rights reserved. +// + +import UIKit +import SwiftUI + +import AltSign +import AltStoreCore +import Roxas + +private final class AltIcon: Decodable +{ + var name: String + var imageName: String + + private enum CodingKeys: String, CodingKey + { + case name + case imageName + } + + required init(from decoder: Decoder) throws + { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + self.imageName = try container.decode(String.self, forKey: .imageName) + } +} + +extension AltAppIconsViewController +{ + private enum Section: String, CaseIterable, Decodable, CodingKeyRepresentable + { + case modern + case inset + case classic + + var localizedName: String { + switch self + { + case .modern: return NSLocalizedString("Modern", comment: "") + case .inset: return NSLocalizedString("Inset", comment: "") + case .classic: return NSLocalizedString("Classic", comment: "") + } + } + } +} + +class AltAppIconsViewController: UICollectionViewController +{ + private lazy var dataSource = self.makeDataSource() + + private var iconsBySection = [Section: [AltIcon]]() + + private var headerRegistration: UICollectionView.SupplementaryRegistration! + + override func viewDidLoad() + { + super.viewDidLoad() + + self.title = NSLocalizedString("Change App Icon", comment: "") + + let collectionViewLayout = self.makeLayout() + self.collectionView.collectionViewLayout = collectionViewLayout + + self.collectionView.backgroundColor = UIColor(resource: .settingsBackground) + + do + { + let fileURL = Bundle.main.url(forResource: "AltIcons", withExtension: "plist")! + let data = try Data(contentsOf: fileURL) + + let icons = try PropertyListDecoder().decode([Section: [AltIcon]].self, from: data) + self.iconsBySection = icons + } + catch + { + Logger.main.error("Failed to load alternate icons. \(error.localizedDescription, privacy: .public)") + } + + self.dataSource.proxy = self + self.collectionView.dataSource = self.dataSource + + self.collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) + self.collectionView.register(UICollectionViewListCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: UICollectionView.elementKindSectionHeader) + + self.headerRegistration = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, elementKind, indexPath) in + let section = Section.allCases[indexPath.section] + + let font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).bolded(), size: 0.0) + + var configuration = UIListContentConfiguration.cell() + configuration.text = section.localizedName + configuration.textProperties.font = font + configuration.textProperties.color = .white.withAlphaComponent(0.8) + headerView.contentConfiguration = configuration + + headerView.backgroundConfiguration = .clear() + } + } +} + +private extension AltAppIconsViewController +{ + func makeLayout() -> UICollectionViewCompositionalLayout + { + var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + configuration.showsSeparators = true + configuration.backgroundColor = .clear + configuration.headerMode = .supplementary + + let layout = UICollectionViewCompositionalLayout.list(using: configuration) + return layout + } + + func makeDataSource() -> RSTCompositeCollectionViewDataSource + { + let dataSources = Section.allCases.compactMap { self.iconsBySection[$0] }.filter { !$0.isEmpty }.map { icons in + let dataSource = RSTArrayCollectionViewDataSource(items: icons) + return dataSource + } + + let dataSource = RSTCompositeCollectionViewDataSource(dataSources: dataSources) + dataSource.cellConfigurationHandler = { cell, icon, indexPath in + let cell = cell as! UICollectionViewListCell + + let imageWidth = 44.0 + let font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).bolded(), size: 0.0) + + var config = cell.defaultContentConfiguration() + config.text = icon.name + config.textProperties.font = font + config.textProperties.color = .label + + let image = UIImage(named: icon.imageName) + config.image = image + config.imageProperties.maximumSize = CGSize(width: imageWidth, height: imageWidth) + config.imageProperties.cornerRadius = imageWidth / 5.0 // Copied from AppIconImageView + + cell.contentConfiguration = config + + var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell() + backgroundConfiguration.backgroundColorTransformer = UIConfigurationColorTransformer { [weak cell] c in + if let state = cell?.configurationState, state.isHighlighted + { + // Highlighted, so use darker white for background. + return .white.withAlphaComponent(0.4) + } + + return .white.withAlphaComponent(0.25) + } + cell.backgroundConfiguration = backgroundConfiguration + + // Ensure text is legible on green background. + cell.overrideUserInterfaceStyle = .dark + } + + return dataSource + } +} + +extension AltAppIconsViewController +{ + override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView + { + let headerView = self.collectionView.dequeueConfiguredReusableSupplementary(using: self.headerRegistration, for: indexPath) + return headerView + } + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) + { + self.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredVertically) + + let icon = self.dataSource.item(at: indexPath) + guard UIApplication.shared.alternateIconName != icon.imageName else { return } + + // If assigning primary icon, pass "nil" as alternate icon name. + let imageName = (icon.imageName == "AppIcon") ? nil : icon.imageName + UIApplication.shared.setAlternateIconName(imageName) { error in + if let error + { + let alertController = UIAlertController(title: NSLocalizedString("Unable to Change App Icon", comment: ""), + message: error.localizedDescription, + preferredStyle: .alert) + alertController.addAction(.ok) + self.present(alertController, animated: true) + } + + if let selectedIndexPath = self.collectionView.indexPathsForSelectedItems?.first + { + self.collectionView.deselectItem(at: selectedIndexPath, animated: true) + } + } + } +} + +@available(iOS 17, *) +#Preview(traits: .portrait) { + let altAppIconsViewController = AltAppIconsViewController(collectionViewLayout: UICollectionViewFlowLayout()) + + let navigationController = UINavigationController(rootViewController: altAppIconsViewController) + return navigationController +} diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 40974b28..717e67a4 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -21,7 +21,7 @@ - +