specific carplay ui

This commit is contained in:
leminlimez committed 2025-06-19 04:23:26 -04:00
1 parent 9d8aef4039
commit 069d5cf1c3
8 files changed
+306 -6

No files matched your search

+19 -2
View File
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */; }; 6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */; };
6F09BA332E00808300CDE89C /* Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09BA322E00808300CDE89C /* Dynamic */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@@ -30,6 +31,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6F09BA332E00808300CDE89C /* Dynamic in Frameworks */,
6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */, 6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -74,6 +76,7 @@
name = "Pocket Poster"; name = "Pocket Poster";
packageProductDependencies = ( packageProductDependencies = (
6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */, 6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */,
6F09BA322E00808300CDE89C /* Dynamic */,
); );
productName = "Pocket Poster"; productName = "Pocket Poster";
productReference = 6F09B79D2DEB694B00CDE89C /* Pocket Poster.app */; productReference = 6F09B79D2DEB694B00CDE89C /* Pocket Poster.app */;
@@ -105,6 +108,7 @@
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = ( packageReferences = (
6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */, 6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */,
); );
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = 6F09B79E2DEB694B00CDE89C /* Products */; productRefGroup = 6F09B79E2DEB694B00CDE89C /* Products */;
@@ -276,7 +280,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -308,7 +312,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -354,6 +358,14 @@
minimumVersion = 0.9.19; minimumVersion = 0.9.19;
}; };
}; };
6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mhdhejazi/Dynamic";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.0;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@@ -362,6 +374,11 @@
package = 6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */; package = 6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */;
productName = ZIPFoundation; productName = ZIPFoundation;
}; };
6F09BA322E00808300CDE89C /* Dynamic */ = {
isa = XCSwiftPackageProductDependency;
package = 6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */;
productName = Dynamic;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 6F09B7952DEB694B00CDE89C /* Project object */; rootObject = 6F09B7952DEB694B00CDE89C /* Project object */;
@@ -1,6 +1,15 @@
{ {
"originHash" : "d0d4edfdf2bf3cd05b3ba2dec0af1a9c271c93f944cbba8677cc647f74a6b323", "originHash" : "b781b75a76813cee1292db0b540a9eecd3df93961ced8a0063095503735dadb2",
"pins" : [ "pins" : [
{
"identity" : "dynamic",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mhdhejazi/Dynamic",
"state" : {
"revision" : "ab9a2570862d54aed2663691bb767f881226a12f",
"version" : "1.2.0"
}
},
{ {
"identity" : "zipfoundation", "identity" : "zipfoundation",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -0,0 +1,28 @@
//
// CPBitmapHandler.swift
// Pocket Poster
//
// Created by lemin on 6/16/25.
//
import UIKit
import Dynamic
class CPBitmapHandler {
static func resizeAndSave(image: UIImage, to url: URL, size: CGSize) throws {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
image.draw(in: CGRect(origin: CGPoint.zero, size: size))
let resizedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
try? FileManager.default.removeItem(at: url)
resizedImage.writeToCPBitmapFile(to: url.path() as NSString)
}
}
extension UIImage {
func writeToCPBitmapFile(to path: NSString) {
Dynamic(self).writeToCPBitmapFile(path, flags: 1)
}
}
@@ -273,6 +273,32 @@ class PosterBoardManager: ObservableObject {
} }
} }
func applyCarPlay(appHash: String, wallpapers: [CarPlayWallpaper]) throws {
// write the image
var toRemove: [URL] = []
for wallpaper in wallpapers {
if let data = wallpaper.selectedImageDataLight, let img = UIImage(data: data) {
let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Light-11.cpbitmap")
img.writeToCPBitmapFile(to: imgURL.path() as NSString)
toRemove.append(imgURL)
}
if let data = wallpaper.selectedImageDataDark, let img = UIImage(data: data) {
let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Dark-11.cpbitmap")
img.writeToCPBitmapFile(to: imgURL.path() as NSString)
toRemove.append(imgURL)
}
}
// symlink and apply
let _ = try SymHandler.createAppSymlink(for: "\(appHash)/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images")
defer {
SymHandler.cleanup()
}
for imgURL in toRemove {
try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
}
}
static func clearCache() throws { static func clearCache() throws {
SymHandler.cleanup() SymHandler.cleanup()
let docDir = SymHandler.getDocumentsDirectory() let docDir = SymHandler.getDocumentsDirectory()
+19
View File
@@ -88,6 +88,9 @@
} }
} }
} }
},
"Apply CarPlay Wallpapers" : {
}, },
"Applying Wallpapers..." : { "Applying Wallpapers..." : {
"localizations" : { "localizations" : {
@@ -132,6 +135,12 @@
} }
} }
} }
},
"CarPlay" : {
},
"CarPlay Wallpapers" : {
}, },
"Clear App Cache" : { "Clear App Cache" : {
"localizations" : { "localizations" : {
@@ -400,6 +409,7 @@
} }
}, },
"Enter App Hash" : { "Enter App Hash" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
@@ -420,6 +430,15 @@
} }
} }
} }
},
"Enter CarPlayWallpaper App Hash" : {
},
"Enter PosterBoard App Hash" : {
},
"Enter your CarPlayWallpaper app hash in Settings." : {
}, },
"Enter your PosterBoard app hash in Settings." : { "Enter your PosterBoard app hash in Settings." : {
"localizations" : { "localizations" : {
@@ -0,0 +1,190 @@
//
// CarPlayView.swift
// Pocket Poster
//
// Created by lemin on 6/19/25.
//
import SwiftUI
import PhotosUI
struct CarPlayWallpaper: Identifiable {
var id = UUID()
var name: String
var lightImage: UIImage
var darkImage: UIImage
var selectedImageDataLight: Data?
var selectedImageDataDark: Data?
var changed: Bool
}
struct CarPlayView: View {
@State var wallpapers: [CarPlayWallpaper] = []
@State var didChange: Bool = false
@State var showDark: Bool = false
@AppStorage("cpHash") var cpHash: String = ""
@ObservedObject var pbManager = PosterBoardManager.shared
var body: some View {
NavigationStack {
ScrollView {
if wallpapers.isEmpty {
ProgressView()
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150, maximum: 250))]) {
ForEach($wallpapers) { wallpaper in
WallpaperView(wallpaper: wallpaper, didChange: $didChange, showDark: $showDark)
}
}
.padding()
}
if cpHash == "" {
Text("Enter your CarPlayWallpaper app hash in Settings.")
.padding()
} else if didChange {
Button(action: {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
UIApplication.shared.alert(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
DispatchQueue.global(qos: .userInitiated).async {
do {
try pbManager.applyCarPlay(appHash: cpHash, wallpapers: wallpapers)
SymHandler.cleanup() // just to be extra sure
try? FileManager.default.removeItem(at: pbManager.getTendiesStoreURL())
UIApplication.shared.dismissAlert(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
pbManager.selectedTendies.removeAll()
Haptic.shared.notify(.success)
UIApplication.shared.confirmAlert(title: NSLocalizedString("Success!", comment: ""), body: NSLocalizedString("The PosterBoard app will now open. Please close it from the app switcher.", comment: ""), onOK: {
if !pbManager.openPosterBoard() {
UIApplication.shared.confirmAlert(title: NSLocalizedString("Falling Back to Shortcut", comment: ""), body: NSLocalizedString("PosterBoard failed to open directly. The fallback shortcut will now be opened.", comment: ""), onOK: {
pbManager.runShortcut(named: "PosterBoard")
}, noCancel: true)
}
}, noCancel: true)
})
} catch CocoaError.fileWriteUnknown {
presentError(ApplyError.wrongAppHash)
} catch CocoaError.fileWriteFileExists {
presentError(ApplyError.collectionsNeedsReset)
} catch {
print(error.localizedDescription)
presentError(ApplyError.unexpected(info: error.localizedDescription))
}
}
}) {
Label("Apply CarPlay Wallpapers", systemImage: "checkmark.circle")
}
.buttonStyle(TintedButton(color: .blue, fullwidth: true))
.padding()
}
}
.navigationTitle("CarPlay Wallpapers")
.toolbar {
ToolbarItem(placement: .topBarTrailing, content: {
Button(action: {
withAnimation {
showDark.toggle()
}
}) {
Image(systemName: showDark ? "moon" : "sun.max")
}
})
}
}
.onAppear {
if wallpapers.isEmpty {
// load the wallpapers
let frameworkPath = "/System/Library/PrivateFrameworks/CarPlayUIServices.framework"
do {
for file in try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: frameworkPath), includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
if file.path().hasSuffix("-Light.heic") {
let imgData = try Data(contentsOf: file)
if let wpImg = UIImage(data: imgData) {
var darkImg = wpImg
if let darkData = try? Data(contentsOf: URL(fileURLWithPath: file.path().replacingOccurrences(of: "-Light.heic", with: "-Dark.heic"))) {
darkImg = UIImage(data: darkData) ?? wpImg
}
let name = file.lastPathComponent.replacingOccurrences(of: "-Light.heic", with: "")
wallpapers.append(.init(name: name, lightImage: wpImg, darkImage: darkImg, changed: false))
}
}
}
} catch {
print(error.localizedDescription)
}
}
}
}
func presentError(_ error: ApplyError) {
SymHandler.cleanup()
UIApplication.shared.dismissAlert(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
Haptic.shared.notify(.error)
UIApplication.shared.alert(body: error.localizedDescription)
})
}
}
struct WallpaperView: View {
@Binding var wallpaper: CarPlayWallpaper
@Binding var didChange: Bool
@Binding var showDark: Bool
@State private var selectedItem: PhotosPickerItem? = nil
var body: some View {
PhotosPicker(
selection: $selectedItem,
matching: .images,
photoLibrary: .shared()) {
VStack {
if showDark {
if let data = wallpaper.selectedImageDataDark, let img = UIImage(data: data) {
WallpaperImageView(img: img)
} else {
WallpaperImageView(img: wallpaper.darkImage)
}
} else {
if let data = wallpaper.selectedImageDataLight, let img = UIImage(data: data) {
WallpaperImageView(img: img)
} else {
WallpaperImageView(img: wallpaper.lightImage)
}
}
}
}
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(.gray, lineWidth: 2)
}
.onChange(of: selectedItem) { newItem in
Task {
// Retrieve selected asset in the form of Data
if let data = try? await newItem?.loadTransferable(type: Data.self) {
if showDark {
wallpaper.selectedImageDataDark = data
} else {
wallpaper.selectedImageDataLight = data
}
didChange = true
}
}
}
}
}
struct WallpaperImageView: View {
var img: UIImage
var body: some View {
Image(uiImage: img)
.resizable()
.aspectRatio(1.25, contentMode: .fill)
.cornerRadius(8)
.clipped()
}
}
+4
View File
@@ -18,6 +18,10 @@ struct RootView: View {
.tabItem { .tabItem {
Label("Videos", systemImage: "camera") Label("Videos", systemImage: "camera")
} }
CarPlayView()
.tabItem {
Label("CarPlay", systemImage: "car")
}
} }
} }
} }
+10 -3
View File
@@ -10,6 +10,7 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
// Prefs // Prefs
@AppStorage("pbHash") var pbHash: String = "" @AppStorage("pbHash") var pbHash: String = ""
@AppStorage("cpHash") var cpHash: String = ""
@State var checkingForHash: Bool = false @State var checkingForHash: Bool = false
@State var hashCheckTask: Task<Void, any Error>? = nil @State var hashCheckTask: Task<Void, any Error>? = nil
@@ -18,14 +19,17 @@ struct SettingsView: View {
List { List {
Section { Section {
VStack { VStack {
TextField("Enter App Hash", text: $pbHash) TextField("Enter PosterBoard App Hash", text: $pbHash)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(.body, design: .monospaced))
TextField("Enter CarPlayWallpaper App Hash", text: $cpHash)
.textFieldStyle(RoundedBorderTextFieldStyle()) .textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(.body, design: .monospaced)) .font(.system(.body, design: .monospaced))
HStack { HStack {
Spacer() Spacer()
// Run task to check until file exists from Nugget pc over AFC // Run task to check until file exists from Nugget pc over AFC
Button(action: { Button(action: {
UIApplication.shared.confirmAlert(title: "Waiting for app hash...", body: "Connect your device to Nugget and click the \"Pocket Poster Helper\" button.", confirmTitle: "Cancel", onOK: { UIApplication.shared.confirmAlert(title: "Waiting for app hashes...", body: "Connect your device to Nugget and click the \"Pocket Poster Helper\" button.", confirmTitle: "Cancel", onOK: {
cancelWaitForHash() cancelWaitForHash()
}, noCancel: true) }, noCancel: true)
startWaitForHash() startWaitForHash()
@@ -33,7 +37,7 @@ struct SettingsView: View {
Text("Detect") Text("Detect")
} }
.foregroundStyle(.green) .foregroundStyle(.green)
.onChange(of: checkingForHash) { .onChange(of: checkingForHash) { _ in
if !checkingForHash { if !checkingForHash {
// hide ui alert // hide ui alert
UIApplication.shared.dismissAlert(animated: true) UIApplication.shared.dismissAlert(animated: true)
@@ -137,6 +141,9 @@ struct SettingsView: View {
if let pb = items.first { if let pb = items.first {
pbHash = String(pb) pbHash = String(pb)
} }
if items.count >= 2 {
cpHash = String(items[1])
}
} }
} catch { } catch {
print(error.localizedDescription) print(error.localizedDescription)