mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Adds ability to change sideloaded app icons
This commit is contained in:
@@ -766,10 +766,18 @@ private extension AppManager
|
||||
|
||||
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.
|
||||
downloadingApp = storeApp
|
||||
if let storeApp = installedApp.storeApp, !FileManager.default.fileExists(atPath: installedApp.fileURL.path)
|
||||
{
|
||||
// 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")
|
||||
|
||||
@@ -51,6 +51,8 @@ class MyAppsViewController: UICollectionViewController
|
||||
private var sideloadingProgress: Progress?
|
||||
private var dropDestinationIndexPath: IndexPath?
|
||||
|
||||
private var _imagePickerInstalledApp: InstalledApp?
|
||||
|
||||
// Cache
|
||||
private var cachedUpdateSizes = [String: CGSize]()
|
||||
|
||||
@@ -361,16 +363,16 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||
let fileURL = item.fileURL
|
||||
|
||||
return BlockOperation {
|
||||
guard let application = ALTApplication(fileURL: fileURL) else {
|
||||
completion(nil, OperationError.invalidApp)
|
||||
return
|
||||
RSTAsyncBlockOperation { (operation) in
|
||||
item.managedObjectContext?.perform {
|
||||
item.loadIcon { (result) in
|
||||
switch result
|
||||
{
|
||||
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
|
||||
@@ -435,16 +437,16 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||
let fileURL = item.fileURL
|
||||
|
||||
return BlockOperation {
|
||||
guard let application = ALTApplication(fileURL: fileURL) else {
|
||||
completion(nil, OperationError.invalidApp)
|
||||
return
|
||||
RSTAsyncBlockOperation { (operation) in
|
||||
item.managedObjectContext?.perform {
|
||||
item.loadIcon { (result) in
|
||||
switch result
|
||||
{
|
||||
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
|
||||
@@ -1280,6 +1282,63 @@ private extension MyAppsViewController
|
||||
documentPicker.delegate = self
|
||||
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
|
||||
@@ -1460,9 +1519,9 @@ extension MyAppsViewController
|
||||
@available(iOS 13.0, *)
|
||||
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
|
||||
self.refresh(installedApp)
|
||||
@@ -1492,8 +1551,24 @@ extension MyAppsViewController
|
||||
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 {
|
||||
return [refreshAction]
|
||||
return [refreshAction, changeIconMenu]
|
||||
}
|
||||
|
||||
if installedApp.isActive
|
||||
@@ -1505,6 +1580,8 @@ extension MyAppsViewController
|
||||
actions.append(activateAction)
|
||||
}
|
||||
|
||||
actions.append(changeIconMenu)
|
||||
|
||||
if installedApp.isActive
|
||||
{
|
||||
actions.append(backupAction)
|
||||
@@ -2012,3 +2089,23 @@ extension MyAppsViewController: UIViewControllerPreviewingDelegate
|
||||
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?
|
||||
|
||||
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
|
||||
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<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="isActive" attributeType="Boolean" defaultValueString="YES" 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="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||
<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="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
|
||||
<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 needsResign: Bool
|
||||
@NSManaged public var hasAlternateIcon: Bool
|
||||
|
||||
@NSManaged public var certificateSerialNumber: String?
|
||||
|
||||
@@ -104,6 +105,32 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
self.refreshedDate = provisioningProfile.creationDate
|
||||
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
|
||||
@@ -269,6 +296,12 @@ public extension InstalledApp
|
||||
return installedBackupAppUTI
|
||||
}
|
||||
|
||||
class func alternateIconURL(for app: AppProtocol) -> URL
|
||||
{
|
||||
let installedBackupAppUTI = self.directoryURL(for: app).appendingPathComponent("AltIcon.png")
|
||||
return installedBackupAppUTI
|
||||
}
|
||||
|
||||
var directoryURL: URL {
|
||||
return InstalledApp.directoryURL(for: self)
|
||||
}
|
||||
@@ -288,4 +321,8 @@ public extension InstalledApp
|
||||
var installedBackupAppUTI: String {
|
||||
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