specific carplay ui
This commit is contained in:
1 parent
9d8aef4039
commit
069d5cf1c3
8 files changed
+306
-6
No files matched your search
@@ -8,6 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */; };
|
||||
6F09BA332E00808300CDE89C /* Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09BA322E00808300CDE89C /* Dynamic */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -30,6 +31,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6F09BA332E00808300CDE89C /* Dynamic in Frameworks */,
|
||||
6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -74,6 +76,7 @@
|
||||
name = "Pocket Poster";
|
||||
packageProductDependencies = (
|
||||
6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */,
|
||||
6F09BA322E00808300CDE89C /* Dynamic */,
|
||||
);
|
||||
productName = "Pocket Poster";
|
||||
productReference = 6F09B79D2DEB694B00CDE89C /* Pocket Poster.app */;
|
||||
@@ -105,6 +108,7 @@
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
|
||||
6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 6F09B79E2DEB694B00CDE89C /* Products */;
|
||||
@@ -276,7 +280,7 @@
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -308,7 +312,7 @@
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -354,6 +358,14 @@
|
||||
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 */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -362,6 +374,11 @@
|
||||
package = 6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */;
|
||||
productName = ZIPFoundation;
|
||||
};
|
||||
6F09BA322E00808300CDE89C /* Dynamic */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */;
|
||||
productName = Dynamic;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 6F09B7952DEB694B00CDE89C /* Project object */;
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
{
|
||||
"originHash" : "d0d4edfdf2bf3cd05b3ba2dec0af1a9c271c93f944cbba8677cc647f74a6b323",
|
||||
"originHash" : "b781b75a76813cee1292db0b540a9eecd3df93961ced8a0063095503735dadb2",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "dynamic",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mhdhejazi/Dynamic",
|
||||
"state" : {
|
||||
"revision" : "ab9a2570862d54aed2663691bb767f881226a12f",
|
||||
"version" : "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "zipfoundation",
|
||||
"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 {
|
||||
SymHandler.cleanup()
|
||||
let docDir = SymHandler.getDocumentsDirectory()
|
||||
|
||||
@@ -88,6 +88,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Apply CarPlay Wallpapers" : {
|
||||
|
||||
},
|
||||
"Applying Wallpapers..." : {
|
||||
"localizations" : {
|
||||
@@ -132,6 +135,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CarPlay" : {
|
||||
|
||||
},
|
||||
"CarPlay Wallpapers" : {
|
||||
|
||||
},
|
||||
"Clear App Cache" : {
|
||||
"localizations" : {
|
||||
@@ -400,6 +409,7 @@
|
||||
}
|
||||
},
|
||||
"Enter App Hash" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"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." : {
|
||||
"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()
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ struct RootView: View {
|
||||
.tabItem {
|
||||
Label("Videos", systemImage: "camera")
|
||||
}
|
||||
CarPlayView()
|
||||
.tabItem {
|
||||
Label("CarPlay", systemImage: "car")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct SettingsView: View {
|
||||
// Prefs
|
||||
@AppStorage("pbHash") var pbHash: String = ""
|
||||
@AppStorage("cpHash") var cpHash: String = ""
|
||||
|
||||
@State var checkingForHash: Bool = false
|
||||
@State var hashCheckTask: Task<Void, any Error>? = nil
|
||||
@@ -18,14 +19,17 @@ struct SettingsView: View {
|
||||
List {
|
||||
Section {
|
||||
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())
|
||||
.font(.system(.body, design: .monospaced))
|
||||
HStack {
|
||||
Spacer()
|
||||
// Run task to check until file exists from Nugget pc over AFC
|
||||
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()
|
||||
}, noCancel: true)
|
||||
startWaitForHash()
|
||||
@@ -33,7 +37,7 @@ struct SettingsView: View {
|
||||
Text("Detect")
|
||||
}
|
||||
.foregroundStyle(.green)
|
||||
.onChange(of: checkingForHash) {
|
||||
.onChange(of: checkingForHash) { _ in
|
||||
if !checkingForHash {
|
||||
// hide ui alert
|
||||
UIApplication.shared.dismissAlert(animated: true)
|
||||
@@ -137,6 +141,9 @@ struct SettingsView: View {
|
||||
if let pb = items.first {
|
||||
pbHash = String(pb)
|
||||
}
|
||||
if items.count >= 2 {
|
||||
cpHash = String(items[1])
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
|
||||
Reference in new issue
Block a user