diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 6c28a57..d247055 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ /* Begin PBXBuildFile section */ 1FA4ABA79AB72914FE414A61 /* libPods-Delta.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */; }; 87343D7B985519A5890A61C6 /* libPods-DeltaPreviews.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E3E5A45AB20C8A87754453B /* libPods-DeltaPreviews.a */; }; + AC1AE30A2A69BD3A00956EB9 /* AlternateAppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1AE3092A69BD3A00956EB9 /* AlternateAppIcons.swift */; }; + AC1AE30C2A6A068F00956EB9 /* Bundle+AppIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1AE30B2A6A068F00956EB9 /* Bundle+AppIconImage.swift */; }; AC1C991029F8B8C30020E6E4 /* ToastNotificationOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1C990F29F8B8C30020E6E4 /* ToastNotificationOptions.swift */; }; AC1C992729F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1C992629F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift */; }; ACF7E30D29F73D03000FE071 /* GameScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */; }; @@ -272,6 +274,8 @@ 8ECE6641DE30D01EA30FE7F6 /* Pods-DeltaPreviews.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DeltaPreviews.release.xcconfig"; path = "Pods/Target Support Files/Pods-DeltaPreviews/Pods-DeltaPreviews.release.xcconfig"; sourceTree = ""; }; A01281C7023C0041B25963BE /* Pods-DeltaPreviews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DeltaPreviews.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DeltaPreviews/Pods-DeltaPreviews.debug.xcconfig"; sourceTree = ""; }; A19FF50F55441BC2B2248241 /* Pods-Delta.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.release.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.release.xcconfig"; sourceTree = ""; }; + AC1AE3092A69BD3A00956EB9 /* AlternateAppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlternateAppIcons.swift; sourceTree = ""; }; + AC1AE30B2A6A068F00956EB9 /* Bundle+AppIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppIconImage.swift"; sourceTree = ""; }; AC1C990F29F8B8C30020E6E4 /* ToastNotificationOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastNotificationOptions.swift; sourceTree = ""; }; AC1C992629F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+ExperimentalToasts.swift"; sourceTree = ""; }; ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameScreenshots.swift; sourceTree = ""; }; @@ -561,6 +565,7 @@ AC1C992629F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift */, D5CDCCC32A85765900E22131 /* OSLog+Delta.swift */, D5CDCCEA2A8593FC00E22131 /* UserDefaults+Delta.swift */, + AC1AE30B2A6A068F00956EB9 /* Bundle+AppIconImage.swift */, ); path = Extensions; sourceTree = ""; @@ -1120,6 +1125,7 @@ ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */, AC1C990F29F8B8C30020E6E4 /* ToastNotificationOptions.swift */, D5147EC72A817B4A00D6CD64 /* ReviewSaveStatesOptions.swift */, + AC1AE3092A69BD3A00956EB9 /* AlternateAppIcons.swift */, ); path = Features; sourceTree = ""; @@ -1636,8 +1642,10 @@ BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */, D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */, BFDCA1E9244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel in Sources */, + AC1AE30A2A69BD3A00956EB9 /* AlternateAppIcons.swift in Sources */, D5A817B329DF6C6C00904AFE /* ExperimentalFeatures.swift in Sources */, BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */, + AC1AE30C2A6A068F00956EB9 /* Bundle+AppIconImage.swift in Sources */, BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */, BF8A333421A484A000A42FD4 /* BadgedTableViewCell.swift in Sources */, D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */, @@ -1919,6 +1927,7 @@ baseConfigurationReference = C786AF1D2DDB6223BE2063CC /* Pods-Delta.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CLANG_MODULES_AUTOLINK = NO; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -1952,6 +1961,7 @@ baseConfigurationReference = A19FF50F55441BC2B2248241 /* Pods-Delta.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CLANG_MODULES_AUTOLINK = NO; CODE_SIGN_IDENTITY = "iPhone Developer"; diff --git a/Delta/Experimental Features/ExperimentalFeatures.swift b/Delta/Experimental Features/ExperimentalFeatures.swift index c84a5a8..571010f 100644 --- a/Delta/Experimental Features/ExperimentalFeatures.swift +++ b/Delta/Experimental Features/ExperimentalFeatures.swift @@ -41,6 +41,11 @@ struct ExperimentalFeatures: FeatureContainer options: ReviewSaveStatesOptions()) var reviewSaveStates + @Feature(name: "Alternate App Icon", + description: "Change the app icon.", + options: AlternateAppIconOptions()) + var alternateAppIcons + private init() { self.prepareFeatures() diff --git a/Delta/Experimental Features/Features/AlternateAppIcons.swift b/Delta/Experimental Features/Features/AlternateAppIcons.swift new file mode 100644 index 0000000..e8910ea --- /dev/null +++ b/Delta/Experimental Features/Features/AlternateAppIcons.swift @@ -0,0 +1,124 @@ +// +// AlternateAppIcons.swift +// Delta +// +// Created by Chris Rittenhouse on 5/2/23. +// Copyright © 2023 LitRitt. All rights reserved. +// + +import SwiftUI + +import DeltaFeatures + +enum AppIcon: String, CaseIterable, CustomStringConvertible, Identifiable +{ + case normal = "Default" + case gba4ios = "GBA4iOS" + case inverted = "Inverted" + case pixelated = "Pixelated" + case skin = "Controller Skin" + + var description: String { + return self.rawValue + } + + var id: String { + return self.rawValue + } + + var author: String { + switch self + { + case .normal: return "Caroline Moore" + case .gba4ios: return "Paul Thorsen" + case .inverted, .skin, .pixelated: return "LitRitt" + } + } + + var assetName: String { + switch self + { + case .normal: return "AppIcon" + case .gba4ios: return "IconGBA4iOS" + case .inverted: return "IconInverted" + case .pixelated: return "IconPixelated" + case .skin: return "IconSkin" + } + } +} + +extension AppIcon: Equatable +{ + static func == (lhs: AppIcon, rhs: AppIcon) -> Bool + { + return lhs.description == rhs.description + } +} + +extension AppIcon: LocalizedOptionValue +{ + var localizedDescription: Text { + Text(self.description) + } +} + +struct AlternateAppIconOptions +{ + @Option(name: "Alternate App Icon", + description: "Choose from alternate app icons created by the community.", + detailView: { value in + List { + ForEach(AppIcon.allCases) { icon in + HStack { + if icon == value.wrappedValue + { + Text("✓") + } + icon.localizedDescription + Text("- by \(icon.author)") + .font(.system(size: 15)) + .foregroundColor(.gray) + Spacer() + Image(uiImage: Bundle.appIcon(for: icon) ?? UIImage()) + .cornerRadius(13) + } + .contentShape(Rectangle()) + .onTapGesture { + value.wrappedValue = icon + } + } + } + .onChange(of: value.wrappedValue) { _ in + updateAppIcon() + } + .displayInline() + }) + var icon: AppIcon = .normal +} + +extension AlternateAppIconOptions +{ + static func updateAppIcon() + { + // Get current icon + let currentIcon = UIApplication.shared.alternateIconName + + // Apply chosen icon if feature is enabled + if ExperimentalFeatures.shared.alternateAppIcons.isEnabled + { + let icon = ExperimentalFeatures.shared.alternateAppIcons.icon + + // Only apply new icon if it's not already the current icon + switch icon + { + case .normal: if currentIcon != nil { UIApplication.shared.setAlternateIconName(nil) } // Default app icon + default: if currentIcon != icon.assetName { UIApplication.shared.setAlternateIconName(icon.assetName) } // Alternate app icon + } + } + else + { + // Remove alternate icons if feature is disabled + if currentIcon != nil { UIApplication.shared.setAlternateIconName(nil) } + } + } +} diff --git a/Delta/Extensions/Bundle+AppIconImage.swift b/Delta/Extensions/Bundle+AppIconImage.swift new file mode 100644 index 0000000..a28b5c5 --- /dev/null +++ b/Delta/Extensions/Bundle+AppIconImage.swift @@ -0,0 +1,34 @@ +// +// Bundle+AppIconImage.swift +// Delta +// +// Created by Chris Rittenhouse on 7/20/23. +// Copyright © 2023 LitRitt. All rights reserved. +// + +import UIKit + +extension Bundle +{ + static func appIcon(for icon: AppIcon = .normal) -> UIImage? { + guard let appIcons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any] else { return nil } + + switch icon + { + case .normal: + guard let primaryAppIcon = appIcons["CFBundlePrimaryIcon"] as? [String: Any], + let appIconFiles = primaryAppIcon["CFBundleIconFiles"] as? [String], + let appIcon = appIconFiles.first else { return nil } + + return UIImage(named:appIcon) + + default: + guard let alternateAppIcons = appIcons["CFBundleAlternateIcons"] as? [String: Any], + let alternateAppIcon = alternateAppIcons[icon.assetName] as? [String: Any], + let appIconFiles = alternateAppIcon["CFBundleIconFiles"] as? [String], + let appIcon = appIconFiles.first else { return nil } + + return UIImage(named:appIcon) + } + } +} diff --git a/Resources/Assets.xcassets/Alternate Icons/Contents.json b/Resources/Assets.xcassets/Alternate Icons/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Resources/Assets.xcassets/Alternate Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Assets.xcassets/Alternate Icons/IconGBA4iOS.appiconset/Contents.json b/Resources/Assets.xcassets/Alternate Icons/IconGBA4iOS.appiconset/Contents.json new file mode 100644 index 0000000..2482888 --- /dev/null +++ b/Resources/Assets.xcassets/Alternate Icons/IconGBA4iOS.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "GBA4iOS.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Assets.xcassets/Alternate Icons/IconGBA4iOS.appiconset/GBA4iOS.png b/Resources/Assets.xcassets/Alternate Icons/IconGBA4iOS.appiconset/GBA4iOS.png new file mode 100644 index 0000000..672ec57 Binary files /dev/null and b/Resources/Assets.xcassets/Alternate Icons/IconGBA4iOS.appiconset/GBA4iOS.png differ diff --git a/Resources/Assets.xcassets/Alternate Icons/IconInverted.appiconset/Contents.json b/Resources/Assets.xcassets/Alternate Icons/IconInverted.appiconset/Contents.json new file mode 100644 index 0000000..f33ef45 --- /dev/null +++ b/Resources/Assets.xcassets/Alternate Icons/IconInverted.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Inverted.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Assets.xcassets/Alternate Icons/IconInverted.appiconset/Inverted.png b/Resources/Assets.xcassets/Alternate Icons/IconInverted.appiconset/Inverted.png new file mode 100644 index 0000000..7d56e24 Binary files /dev/null and b/Resources/Assets.xcassets/Alternate Icons/IconInverted.appiconset/Inverted.png differ diff --git a/Resources/Assets.xcassets/Alternate Icons/IconPixelated.appiconset/Contents.json b/Resources/Assets.xcassets/Alternate Icons/IconPixelated.appiconset/Contents.json new file mode 100644 index 0000000..f2956eb --- /dev/null +++ b/Resources/Assets.xcassets/Alternate Icons/IconPixelated.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Pixelated.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Assets.xcassets/Alternate Icons/IconPixelated.appiconset/Pixelated.png b/Resources/Assets.xcassets/Alternate Icons/IconPixelated.appiconset/Pixelated.png new file mode 100644 index 0000000..fd0a791 Binary files /dev/null and b/Resources/Assets.xcassets/Alternate Icons/IconPixelated.appiconset/Pixelated.png differ diff --git a/Resources/Assets.xcassets/Alternate Icons/iConSkin.appiconset/Contents.json b/Resources/Assets.xcassets/Alternate Icons/iConSkin.appiconset/Contents.json new file mode 100644 index 0000000..5a207ad --- /dev/null +++ b/Resources/Assets.xcassets/Alternate Icons/iConSkin.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Skin.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Assets.xcassets/Alternate Icons/iConSkin.appiconset/Skin.png b/Resources/Assets.xcassets/Alternate Icons/iConSkin.appiconset/Skin.png new file mode 100644 index 0000000..a77d96f Binary files /dev/null and b/Resources/Assets.xcassets/Alternate Icons/iConSkin.appiconset/Skin.png differ