mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-20 04:03:26 +01:00
Adds ability to change sideloaded app icons
This commit is contained in:
@@ -766,10 +766,18 @@ private extension AppManager
|
|||||||
|
|
||||||
var downloadingApp = app
|
var downloadingApp = app
|
||||||
|
|
||||||
if let installedApp = app as? InstalledApp, let storeApp = installedApp.storeApp, !FileManager.default.fileExists(atPath: installedApp.fileURL.path)
|
if let installedApp = app as? InstalledApp
|
||||||
{
|
{
|
||||||
// Cached app has been deleted, so we need to redownload it.
|
if let storeApp = installedApp.storeApp, !FileManager.default.fileExists(atPath: installedApp.fileURL.path)
|
||||||
downloadingApp = storeApp
|
{
|
||||||
|
// Cached app has been deleted, so we need to redownload it.
|
||||||
|
downloadingApp = storeApp
|
||||||
|
}
|
||||||
|
|
||||||
|
if installedApp.hasAlternateIcon
|
||||||
|
{
|
||||||
|
context.alternateIconURL = installedApp.alternateIconURL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let downloadedAppURL = context.temporaryDirectory.appendingPathComponent("Cached.app")
|
let downloadedAppURL = context.temporaryDirectory.appendingPathComponent("Cached.app")
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class MyAppsViewController: UICollectionViewController
|
|||||||
private var sideloadingProgress: Progress?
|
private var sideloadingProgress: Progress?
|
||||||
private var dropDestinationIndexPath: IndexPath?
|
private var dropDestinationIndexPath: IndexPath?
|
||||||
|
|
||||||
|
private var _imagePickerInstalledApp: InstalledApp?
|
||||||
|
|
||||||
// Cache
|
// Cache
|
||||||
private var cachedUpdateSizes = [String: CGSize]()
|
private var cachedUpdateSizes = [String: CGSize]()
|
||||||
|
|
||||||
@@ -361,16 +363,16 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||||
let fileURL = item.fileURL
|
RSTAsyncBlockOperation { (operation) in
|
||||||
|
item.managedObjectContext?.perform {
|
||||||
return BlockOperation {
|
item.loadIcon { (result) in
|
||||||
guard let application = ALTApplication(fileURL: fileURL) else {
|
switch result
|
||||||
completion(nil, OperationError.invalidApp)
|
{
|
||||||
return
|
case .failure(let error): completion(nil, error)
|
||||||
|
case .success(let image): completion(image, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon = application.icon
|
|
||||||
completion(icon, nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
@@ -435,16 +437,16 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||||
let fileURL = item.fileURL
|
RSTAsyncBlockOperation { (operation) in
|
||||||
|
item.managedObjectContext?.perform {
|
||||||
return BlockOperation {
|
item.loadIcon { (result) in
|
||||||
guard let application = ALTApplication(fileURL: fileURL) else {
|
switch result
|
||||||
completion(nil, OperationError.invalidApp)
|
{
|
||||||
return
|
case .failure(let error): completion(nil, error)
|
||||||
|
case .success(let image): completion(image, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon = application.icon
|
|
||||||
completion(icon, nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
@@ -1280,6 +1282,63 @@ private extension MyAppsViewController
|
|||||||
documentPicker.delegate = self
|
documentPicker.delegate = self
|
||||||
self.present(documentPicker, animated: true, completion: nil)
|
self.present(documentPicker, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func chooseIcon(for installedApp: InstalledApp)
|
||||||
|
{
|
||||||
|
self._imagePickerInstalledApp = installedApp
|
||||||
|
|
||||||
|
let imagePicker = UIImagePickerController()
|
||||||
|
imagePicker.delegate = self
|
||||||
|
imagePicker.allowsEditing = true
|
||||||
|
self.present(imagePicker, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeIcon(for installedApp: InstalledApp, to image: UIImage?)
|
||||||
|
{
|
||||||
|
// Remove previous icon from cache.
|
||||||
|
self.activeAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
|
||||||
|
self.inactiveAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
|
||||||
|
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||||
|
tempApp.needsResign = true
|
||||||
|
tempApp.hasAlternateIcon = (image != nil)
|
||||||
|
|
||||||
|
if let image = image
|
||||||
|
{
|
||||||
|
guard let icon = image.resizing(toFill: CGSize(width: 256, height: 256)),
|
||||||
|
let iconData = icon.pngData()
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
try iconData.write(to: tempApp.alternateIconURL, options: .atomic)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try FileManager.default.removeItem(at: tempApp.alternateIconURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
try context.save()
|
||||||
|
|
||||||
|
if tempApp.isActive
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.refresh(installedApp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to change app icon.", error)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension MyAppsViewController
|
private extension MyAppsViewController
|
||||||
@@ -1460,9 +1519,9 @@ extension MyAppsViewController
|
|||||||
@available(iOS 13.0, *)
|
@available(iOS 13.0, *)
|
||||||
extension MyAppsViewController
|
extension MyAppsViewController
|
||||||
{
|
{
|
||||||
private func actions(for installedApp: InstalledApp) -> [UIAction]
|
private func actions(for installedApp: InstalledApp) -> [UIMenuElement]
|
||||||
{
|
{
|
||||||
var actions = [UIAction]()
|
var actions = [UIMenuElement]()
|
||||||
|
|
||||||
let refreshAction = UIAction(title: NSLocalizedString("Refresh", comment: ""), image: UIImage(systemName: "arrow.clockwise")) { (action) in
|
let refreshAction = UIAction(title: NSLocalizedString("Refresh", comment: ""), image: UIImage(systemName: "arrow.clockwise")) { (action) in
|
||||||
self.refresh(installedApp)
|
self.refresh(installedApp)
|
||||||
@@ -1492,8 +1551,24 @@ extension MyAppsViewController
|
|||||||
self.restore(installedApp)
|
self.restore(installedApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let chooseIconAction = UIAction(title: NSLocalizedString("Photos", comment: ""), image: UIImage(systemName: "photo")) { (action) in
|
||||||
|
self.chooseIcon(for: installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
let removeIconAction = UIAction(title: NSLocalizedString("Remove Custom Icon", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive]) { (action) in
|
||||||
|
self.changeIcon(for: installedApp, to: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var changeIconActions = [chooseIconAction]
|
||||||
|
if installedApp.hasAlternateIcon
|
||||||
|
{
|
||||||
|
changeIconActions.append(removeIconAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
let changeIconMenu = UIMenu(title: NSLocalizedString("Change Icon", comment: ""), image: UIImage(systemName: "photo"), children: changeIconActions)
|
||||||
|
|
||||||
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
|
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
|
||||||
return [refreshAction]
|
return [refreshAction, changeIconMenu]
|
||||||
}
|
}
|
||||||
|
|
||||||
if installedApp.isActive
|
if installedApp.isActive
|
||||||
@@ -1505,6 +1580,8 @@ extension MyAppsViewController
|
|||||||
actions.append(activateAction)
|
actions.append(activateAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actions.append(changeIconMenu)
|
||||||
|
|
||||||
if installedApp.isActive
|
if installedApp.isActive
|
||||||
{
|
{
|
||||||
actions.append(backupAction)
|
actions.append(backupAction)
|
||||||
@@ -2012,3 +2089,23 @@ extension MyAppsViewController: UIViewControllerPreviewingDelegate
|
|||||||
self.performSegue(withIdentifier: "showUpdate", sender: cell)
|
self.performSegue(withIdentifier: "showUpdate", sender: cell)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MyAppsViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate
|
||||||
|
{
|
||||||
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any])
|
||||||
|
{
|
||||||
|
defer {
|
||||||
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
self._imagePickerInstalledApp = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let image = info[.editedImage] as? UIImage, let installedApp = self._imagePickerInstalledApp else { return }
|
||||||
|
self.changeIcon(for: installedApp, to: image)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController)
|
||||||
|
{
|
||||||
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
self._imagePickerInstalledApp = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,4 +114,6 @@ class InstallAppOperationContext: AppOperationContext
|
|||||||
private var installedAppContext: NSManagedObjectContext?
|
private var installedAppContext: NSManagedObjectContext?
|
||||||
|
|
||||||
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
||||||
|
|
||||||
|
var alternateIconURL: URL?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,6 +191,22 @@ private extension ResignAppOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let iconScale = Int(UIScreen.main.scale)
|
||||||
|
|
||||||
|
if let alternateIconURL = self.context.alternateIconURL,
|
||||||
|
case let data = try Data(contentsOf: alternateIconURL),
|
||||||
|
let image = UIImage(data: data),
|
||||||
|
let icon = image.resizing(toFill: CGSize(width: 60 * iconScale, height: 60 * iconScale)),
|
||||||
|
let iconData = icon.pngData()
|
||||||
|
{
|
||||||
|
let iconName = "AltIcon"
|
||||||
|
let iconURL = appBundleURL.appendingPathComponent(iconName + "@\(iconScale)x.png")
|
||||||
|
try iconData.write(to: iconURL, options: .atomic)
|
||||||
|
|
||||||
|
let iconDictionary = ["CFBundlePrimaryIcon": ["CFBundleIconFiles": [iconName]]]
|
||||||
|
additionalValues["CFBundleIcons"] = iconDictionary
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare app
|
// Prepare app
|
||||||
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
@@ -164,7 +165,7 @@
|
|||||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||||
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="253"/>
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="268"/>
|
||||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
|
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
|
||||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
|
|||||||
|
|
||||||
@NSManaged public var isActive: Bool
|
@NSManaged public var isActive: Bool
|
||||||
@NSManaged public var needsResign: Bool
|
@NSManaged public var needsResign: Bool
|
||||||
|
@NSManaged public var hasAlternateIcon: Bool
|
||||||
|
|
||||||
@NSManaged public var certificateSerialNumber: String?
|
@NSManaged public var certificateSerialNumber: String?
|
||||||
|
|
||||||
@@ -104,6 +105,32 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
|
|||||||
self.refreshedDate = provisioningProfile.creationDate
|
self.refreshedDate = provisioningProfile.creationDate
|
||||||
self.expirationDate = provisioningProfile.expirationDate
|
self.expirationDate = provisioningProfile.expirationDate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func loadIcon(completion: @escaping (Result<UIImage?, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let hasAlternateIcon = self.hasAlternateIcon
|
||||||
|
let alternateIconURL = self.alternateIconURL
|
||||||
|
let fileURL = self.fileURL
|
||||||
|
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if hasAlternateIcon,
|
||||||
|
case let data = try Data(contentsOf: alternateIconURL),
|
||||||
|
let icon = UIImage(data: data)
|
||||||
|
{
|
||||||
|
return completion(.success(icon))
|
||||||
|
}
|
||||||
|
|
||||||
|
let application = ALTApplication(fileURL: fileURL)
|
||||||
|
completion(.success(application?.icon))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension InstalledApp
|
public extension InstalledApp
|
||||||
@@ -269,6 +296,12 @@ public extension InstalledApp
|
|||||||
return installedBackupAppUTI
|
return installedBackupAppUTI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class func alternateIconURL(for app: AppProtocol) -> URL
|
||||||
|
{
|
||||||
|
let installedBackupAppUTI = self.directoryURL(for: app).appendingPathComponent("AltIcon.png")
|
||||||
|
return installedBackupAppUTI
|
||||||
|
}
|
||||||
|
|
||||||
var directoryURL: URL {
|
var directoryURL: URL {
|
||||||
return InstalledApp.directoryURL(for: self)
|
return InstalledApp.directoryURL(for: self)
|
||||||
}
|
}
|
||||||
@@ -288,4 +321,8 @@ public extension InstalledApp
|
|||||||
var installedBackupAppUTI: String {
|
var installedBackupAppUTI: String {
|
||||||
return InstalledApp.installedBackupAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
return InstalledApp.installedBackupAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var alternateIconURL: URL {
|
||||||
|
return InstalledApp.alternateIconURL(for: self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
Dependencies/Roxas
vendored
2
Dependencies/Roxas
vendored
Submodule Dependencies/Roxas updated: d5c9a6551d...84645e4318
Reference in New Issue
Block a user