diff --git a/Pocket Poster.xcodeproj/project.pbxproj b/Pocket Poster.xcodeproj/project.pbxproj index c916911..285931e 100644 --- a/Pocket Poster.xcodeproj/project.pbxproj +++ b/Pocket Poster.xcodeproj/project.pbxproj @@ -6,6 +6,10 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */; }; +/* End PBXBuildFile section */ + /* Begin PBXFileReference section */ 6F09B79D2DEB694B00CDE89C /* Pocket Poster.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Pocket Poster.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -23,6 +27,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -65,6 +70,7 @@ ); name = "Pocket Poster"; packageProductDependencies = ( + 6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */, ); productName = "Pocket Poster"; productReference = 6F09B79D2DEB694B00CDE89C /* Pocket Poster.app */; @@ -94,6 +100,9 @@ ); mainGroup = 6F09B7942DEB694B00CDE89C; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 6F09B79E2DEB694B00CDE89C /* Products */; projectDirPath = ""; @@ -177,6 +186,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -234,6 +244,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; @@ -252,13 +263,16 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\""; + DEVELOPMENT_TEAM = 2P4CP47H76; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Pocket-Poster-Info.plist"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -280,13 +294,16 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\""; + DEVELOPMENT_TEAM = 2P4CP47H76; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Pocket-Poster-Info.plist"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -322,6 +339,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/weichsel/ZIPFoundation"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.19; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = 6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */; + productName = ZIPFoundation; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 6F09B7952DEB694B00CDE89C /* Project object */; } diff --git a/Pocket Poster.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Pocket Poster.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..70ae973 --- /dev/null +++ b/Pocket Poster.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "d0d4edfdf2bf3cd05b3ba2dec0af1a9c271c93f944cbba8677cc647f74a6b323", + "pins" : [ + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation", + "state" : { + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" + } + } + ], + "version" : 3 +} diff --git a/Pocket Poster/ContentView.swift b/Pocket Poster/ContentView.swift deleted file mode 100644 index 5a39af5..0000000 --- a/Pocket Poster/ContentView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ContentView.swift -// Pocket Poster -// -// Created by lemin on 5/31/25. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - } - .padding() - } -} - -#Preview { - ContentView() -} diff --git a/Pocket Poster/Controllers/PosterBoardManager.swift b/Pocket Poster/Controllers/PosterBoardManager.swift new file mode 100644 index 0000000..1fd48ad --- /dev/null +++ b/Pocket Poster/Controllers/PosterBoardManager.swift @@ -0,0 +1,142 @@ +// +// PosterBoardManager.swift +// Pocket Poster +// +// Created by lemin on 5/31/25. +// + +import Foundation +import ZIPFoundation + +class PosterBoardManager { + private static func unzipFile(at url: URL) throws -> URL { + let fileName = url.deletingPathExtension().lastPathComponent + let fileData = try Data(contentsOf: url) + let fileManager = FileManager() + + // Write the file to the Documents Directory + let docDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let path = docDir[0] + let url = path.appending(path: fileName) + + // Remove All files in this directory + let existingFiles = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) + for fileUrl in existingFiles + { + try FileManager.default.removeItem(at: fileUrl) + } + + // Save our Zip file + try fileData.write(to: url, options: [.atomic]) + + // Unzip the Zipped Up File + var destinationURL = path + if FileManager.default.fileExists(atPath: url.path()) + { + destinationURL.append(path: "directory") + try fileManager.unzipItem(at: url, to: destinationURL) + } + + return destinationURL + } + + static func getDescriptorsFromTendie(_ url: URL) throws -> URL? { + let unzippedDir = try unzipFile(at: url) + + for dir in try FileManager.default.contentsOfDirectory(at: unzippedDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) { + let fileName = dir.lastPathComponent + if fileName.lowercased() == "descriptor" || fileName.lowercased() == "descriptors" { + return dir + } + } + return nil + } + + static func randomizeWallpaperId(url: URL) throws { + let randomizedID = Int.random(in: 9999...99999) + var files = [URL]() + if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) { + for case let fileURL as URL in enumerator + { + do + { + let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey]) + if fileAttributes.isRegularFile! + { + files.append(fileURL) + } + } + catch + { + print(error, fileURL) + } + } + } + + func setPlistValue(file: String, key: String, value: Any, recursive: Bool = true) { + // thanks gpt + guard let plistData = FileManager.default.contents(atPath: file), + var plist = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: Any] else { + return + } + + plist[key] = value + + guard let updatedData = try? PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) else { + return + } + + do { + try updatedData.write(to: URL(fileURLWithPath: file)) + } catch { + print("Failed to write updated plist: \(error)") + } + } + + for file in files { + switch file.lastPathComponent { + case "com.apple.posterkit.provider.descriptor.identifier": + try String(randomizedID).data(using: .utf8)?.write(to: file) + + case "com.apple.posterkit.provider.contents.userInfo": + setPlistValue(file: file.path(), key: "wallpaperRepresentingIdentifier", value: randomizedID) + + case "Wallpaper.plist": + setPlistValue(file: file.path(), key: "identifier", value: randomizedID, recursive: false) + + default: + continue + } + } + } + + static func applyTendie(_ url: URL, appHash: String) throws { + // scope the resource + let accessing = url.startAccessingSecurityScopedResource() + defer { + if accessing { + url.stopAccessingSecurityScopedResource() + } + } + + guard let descriptors = try getDescriptorsFromTendie(url) else { return } // TODO: Add error handling + // create the folder + for descr in try FileManager.default.contentsOfDirectory(at: descriptors, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) { + if descr.lastPathComponent != "__MACOSX" { + try randomizeWallpaperId(url: descr) + let newURL = SymHandler.getDocumentsDirectory().appendingPathComponent(UUID().uuidString, conformingTo: .directory) + try FileManager.default.moveItem(at: descr, to: newURL) + + let _ = try SymHandler.createDescriptorsSymlink(appHash: appHash) + do { + try FileManager.default.trashItem(at: newURL, resultingItemURL: nil) + } catch { + print(error.localizedDescription) + } + SymHandler.cleanup() + } + } + + try? FileManager.default.removeItem(at: descriptors) + } +} diff --git a/Pocket Poster/Controllers/SymHandler.swift b/Pocket Poster/Controllers/SymHandler.swift new file mode 100644 index 0000000..92902d9 --- /dev/null +++ b/Pocket Poster/Controllers/SymHandler.swift @@ -0,0 +1,49 @@ +// +// SymHandler.swift +// Pocket Poster +// +// Created by lemin on 5/31/25. +// + +import Foundation + +class SymHandler { + // MARK: URL Getter Operations + static func getDocumentsDirectory() -> URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectory = paths[0] + return documentsDirectory + } + + private static func getSymlinkURL() -> URL { + return getDocumentsDirectory().appendingPathComponent(".Trash", conformingTo: .symbolicLink) + } + + // MARK: Symlink Creation + static func createSymlink(to path: String) throws -> URL { + // returns the url of the symlink + let symURL = getSymlinkURL() + cleanup() + + // create the symlink to the hashed app folder + try FileManager.default.createSymbolicLink(at: symURL, withDestinationURL: URL(fileURLWithPath: path, isDirectory: true)) + + return symURL + } + + static func createAppSymlink(for appHash: String) throws -> URL { + return try createSymlink(to: "/var/mobile/Containers/Data/Application/\(appHash)") + } + + static func createDescriptorsSymlink(appHash: String) throws -> URL { + // create a symlink directly to the descriptors + return try createAppSymlink(for: "\(appHash)/Library/Application Support/PRBPosterExtensionDataStore/61/Extensions/com.apple.WallpaperKit.CollectionsPoster/descriptors") + } + + static func cleanup() { + // remove the symlink if it exists + let symURL = getSymlinkURL() + // remove existing symlink + try? FileManager.default.removeItem(at: symURL) + } +} diff --git a/Pocket Poster/Views/ContentView.swift b/Pocket Poster/Views/ContentView.swift new file mode 100644 index 0000000..55378d9 --- /dev/null +++ b/Pocket Poster/Views/ContentView.swift @@ -0,0 +1,69 @@ +// +// ContentView.swift +// Pocket Poster +// +// Created by lemin on 5/31/25. +// + +import SwiftUI +import UniformTypeIdentifiers + +struct ContentView: View { + // Prefs + @AppStorage("pbHash") var pbHash: String = "" + + @State var showTendiesImporter: Bool = false + @State var selectedTendie: URL? = nil + + @State var showErrorAlert = false + @State var lastError: String? + + var body: some View { + VStack { + TextField("App Hash", text: $pbHash) + + Button(action: { + showTendiesImporter.toggle() + }) { + Text("Select Tendie") + } + + if let selectedTendie = selectedTendie { + Text("Selected Tendie: \(selectedTendie)") + if pbHash != "" { + Button(action: { + do { + try PosterBoardManager.applyTendie(selectedTendie, appHash: pbHash) + lastError = "Success. Delete in files app." + showErrorAlert.toggle() + } catch { + lastError = error.localizedDescription + showErrorAlert.toggle() + } + }) { + Text("Apply") + } + } + } + } + .padding() + .fileImporter(isPresented: $showTendiesImporter, allowedContentTypes: [UTType(filenameExtension: "tendies", conformingTo: .data)!], onCompletion: { result in + switch result { + case .success(let url): + selectedTendie = url + case .failure(let error): + lastError = error.localizedDescription + showErrorAlert.toggle() + } + }) + .alert("Error", isPresented: $showErrorAlert) { + Button("OK") {} + } message: { + Text(lastError ?? "???") + } + } +} + +#Preview { + ContentView() +}