27 files changed
+11270
-112
No files matched your search
@@ -17,6 +17,9 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
6F09B79F2DEB694B00CDE89C /* Pocket Poster */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
explicitFolders = (
|
||||
Resources/VideoCAML,
|
||||
);
|
||||
path = "Pocket Poster";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -278,7 +281,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.lemin.Pocket-Poster";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -310,7 +313,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.lemin.Pocket-Poster";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
<key>Pocket Poster.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "POEditor@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -19,7 +19,7 @@ class DownloadManager {
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request) as! (Data, HTTPURLResponse)
|
||||
guard response.statusCode == 200 else { throw URLError(.cannotConnectToHost) }
|
||||
let newURL = PosterBoardManager.getTendiesStoreURL().appendingPathComponent(getWallpaperNameFromURL(string: url.absoluteString))
|
||||
let newURL = PosterBoardManager.shared.getTendiesStoreURL().appendingPathComponent(getWallpaperNameFromURL(string: url.absoluteString))
|
||||
try data.write(to: newURL)
|
||||
return newURL
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class DownloadManager {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
let newURL = PosterBoardManager.getTendiesStoreURL().appendingPathComponent(url.lastPathComponent)
|
||||
let newURL = PosterBoardManager.shared.getTendiesStoreURL().appendingPathComponent(url.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: url, to: newURL)
|
||||
return newURL
|
||||
}
|
||||
|
||||
@@ -9,13 +9,18 @@ import Foundation
|
||||
import ZIPFoundation
|
||||
import UIKit
|
||||
|
||||
class PosterBoardManager {
|
||||
class PosterBoardManager: ObservableObject {
|
||||
static let ShortcutURL = "https://www.icloud.com/shortcuts/a28d2c02ca11453cb5b8f91c12cfa692"
|
||||
static let WallpapersURL = "https://cowabun.ga/wallpapers"
|
||||
|
||||
static let MaxTendies = 10
|
||||
|
||||
static func getTendiesStoreURL() -> URL {
|
||||
static let shared = PosterBoardManager()
|
||||
|
||||
@Published var selectedTendies: [URL] = []
|
||||
@Published var videos: [LoadInfo] = []
|
||||
|
||||
func getTendiesStoreURL() -> URL {
|
||||
let tendiesStoreURL = SymHandler.getDocumentsDirectory().appendingPathComponent("KFC Bucket", conformingTo: .directory)
|
||||
// create it if it doesn't exist
|
||||
if !FileManager.default.fileExists(atPath: tendiesStoreURL.path()) {
|
||||
@@ -24,7 +29,24 @@ class PosterBoardManager {
|
||||
return tendiesStoreURL
|
||||
}
|
||||
|
||||
static func openPosterBoard() -> Bool {
|
||||
func setSystemLanguage(to new_lang: String) -> Bool {
|
||||
var langManager: NSObject = NSObject()
|
||||
if #available(iOS 18.0, *) {
|
||||
guard let obj = objc_getClass("IPSettingsUtilities") as? NSObject else { return false }
|
||||
langManager = obj
|
||||
} else {
|
||||
guard let obj = objc_getClass("PSLanguageSelector") as? NSObject else { return false }
|
||||
langManager = obj
|
||||
}
|
||||
|
||||
if let success = langManager.perform(Selector(("setLanguage:")), with: new_lang) {
|
||||
return success != nil
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func openPosterBoard() -> Bool {
|
||||
guard let obj = objc_getClass("LSApplicationWorkspace") as? NSObject else { return false }
|
||||
let workspace = obj.perform(Selector(("defaultWorkspace")))?.takeUnretainedValue() as? NSObject
|
||||
|
||||
@@ -35,7 +57,7 @@ class PosterBoardManager {
|
||||
return false
|
||||
}
|
||||
|
||||
private static func unzipFile(at url: URL) throws -> URL {
|
||||
private func unzipFile(at url: URL) throws -> URL {
|
||||
let fileName = url.deletingPathExtension().lastPathComponent
|
||||
let fileData = try Data(contentsOf: url)
|
||||
let fileManager = FileManager()
|
||||
@@ -68,13 +90,13 @@ class PosterBoardManager {
|
||||
return destinationURL
|
||||
}
|
||||
|
||||
static func runShortcut(named name: String) {
|
||||
func runShortcut(named name: String) {
|
||||
guard let urlEncodedName = name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
||||
let url = URL(string: "shortcuts://run-shortcut?name=\(name)") else { return }
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
|
||||
static func getDescriptorsFromTendie(_ url: URL) throws -> [String: [URL]]? {
|
||||
func getDescriptorsFromTendie(_ url: URL) throws -> [String: [URL]]? {
|
||||
for dir in try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
|
||||
let fileName = dir.lastPathComponent
|
||||
if fileName.lowercased() == "container" {
|
||||
@@ -99,7 +121,7 @@ class PosterBoardManager {
|
||||
return nil
|
||||
}
|
||||
|
||||
static func randomizeWallpaperId(url: URL) throws {
|
||||
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]) {
|
||||
@@ -157,10 +179,28 @@ class PosterBoardManager {
|
||||
}
|
||||
}
|
||||
|
||||
static func applyTendies(_ urls: [URL], appHash: String) throws {
|
||||
func applyTendies(appHash: String) throws {
|
||||
// organize the descriptors into their respective extensions
|
||||
var extList: [String: [URL]] = [:]
|
||||
for url in urls {
|
||||
// create the video first
|
||||
if videos.count > 0 {
|
||||
extList["com.apple.WallpaperKit.CollectionsPoster"] = []
|
||||
for video in videos {
|
||||
switch video.loadState {
|
||||
case .loaded(let movie):
|
||||
do {
|
||||
let newVideo = try VideoHandler.createCaml(from: movie.url, autoReverses: video.autoReverses)
|
||||
extList["com.apple.WallpaperKit.CollectionsPoster"]?.append(newVideo)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
default:
|
||||
print("Video not loaded!")
|
||||
}
|
||||
}
|
||||
}
|
||||
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Extracting tendies...", comment: "happens when unzipping the files"))
|
||||
for url in selectedTendies {
|
||||
let unzippedDir = try unzipFile(at: url)
|
||||
guard let descriptors = try getDescriptorsFromTendie(unzippedDir) else { continue } // TODO: Add error handling
|
||||
extList.merge(descriptors) { (first, second) in first + second }
|
||||
@@ -188,7 +228,7 @@ class PosterBoardManager {
|
||||
}
|
||||
|
||||
// clean up all possible files
|
||||
for url in urls {
|
||||
for url in selectedTendies {
|
||||
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent("UnzipItems", conformingTo: .directory))
|
||||
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent(url.lastPathComponent))
|
||||
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent(url.deletingPathExtension().lastPathComponent))
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
//
|
||||
// VideoHandler.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/8/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import CoreTransferable
|
||||
|
||||
extension CGImage {
|
||||
var png: Data? {
|
||||
guard let mutableData = CFDataCreateMutable(nil, 0),
|
||||
let destination = CGImageDestinationCreateWithData(mutableData, "public.png" as CFString, 1, nil) else { return nil }
|
||||
CGImageDestinationAddImage(destination, self, nil)
|
||||
guard CGImageDestinationFinalize(destination) else { return nil }
|
||||
return mutableData as Data
|
||||
}
|
||||
var jpg: Data? {
|
||||
let uiimg = UIImage(cgImage: self)
|
||||
return uiimg.jpegData(compressionQuality: 0.7)
|
||||
}
|
||||
}
|
||||
|
||||
struct Movie: Transferable {
|
||||
let url: URL
|
||||
|
||||
static var transferRepresentation: some TransferRepresentation {
|
||||
FileRepresentation(contentType: .movie) { movie in
|
||||
SentTransferredFile(movie.url)
|
||||
} importing: { received in
|
||||
let videoFolder = SymHandler.getDocumentsDirectory().appendingPathComponent("Videos", conformingTo: .directory)
|
||||
if !FileManager.default.fileExists(atPath: videoFolder.path()) {
|
||||
try? FileManager.default.createDirectory(at: videoFolder, withIntermediateDirectories: true)
|
||||
}
|
||||
let copy = videoFolder.appending(path: "\(UUID()).mp4")
|
||||
|
||||
if FileManager.default.fileExists(atPath: copy.path()) {
|
||||
try FileManager.default.removeItem(at: copy)
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: received.file, to: copy)
|
||||
return Self.init(url: copy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LoadState {
|
||||
case unknown, loading, loaded(Movie), failed
|
||||
}
|
||||
|
||||
struct LoadInfo: Identifiable, Equatable {
|
||||
static func == (lhs: LoadInfo, rhs: LoadInfo) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
var id = UUID()
|
||||
var autoReverses: Bool = false
|
||||
var loadState: LoadState
|
||||
}
|
||||
|
||||
class VideoHandler {
|
||||
static let MaxDurationSecs = 12.0
|
||||
|
||||
static func isVideoTooLong(at url: URL) -> Bool {
|
||||
let asset = AVAsset(url: url)
|
||||
|
||||
let duration = asset.duration
|
||||
let durationTime = CMTimeGetSeconds(duration)
|
||||
|
||||
return durationTime > MaxDurationSecs
|
||||
}
|
||||
|
||||
static func createCaml(from url: URL, autoReverses: Bool) throws -> URL {
|
||||
// copy the resources
|
||||
let descrURL = SymHandler.getDocumentsDirectory().appendingPathComponent(UUID().uuidString, conformingTo: .directory)
|
||||
try FileManager.default.createDirectory(at: descrURL, withIntermediateDirectories: false)
|
||||
|
||||
print("pre resource")
|
||||
guard let rc = Bundle.main.url(forResource: "VideoCAML", withExtension: nil) else { throw URLError(.fileDoesNotExist) }
|
||||
print("post resource")
|
||||
try FileManager.default.copyItem(at: rc, to: descrURL.appendingPathComponent("videoCAML"))
|
||||
print("post copy")
|
||||
|
||||
let coreAnimDir = descrURL.appending(path: "videoCAML/versions/1/contents/9183.Custom-810w-1080h@2x~ipad.wallpaper/9183.Custom_Background-810w-1080h@2x~ipad.ca")
|
||||
let assetsFolder = coreAnimDir.appendingPathComponent("assets", conformingTo: .directory)
|
||||
if !FileManager.default.fileExists(atPath: assetsFolder.path()) {
|
||||
try FileManager.default.createDirectory(at: assetsFolder, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
// load the video
|
||||
let asset = AVURLAsset(url: url)
|
||||
|
||||
let assetImageGenerator = AVAssetImageGenerator(asset: asset)
|
||||
assetImageGenerator.appliesPreferredTrackTransform = true
|
||||
assetImageGenerator.apertureMode = AVAssetImageGenerator.ApertureMode.encodedPixels
|
||||
|
||||
// create the caml
|
||||
guard let track = asset.tracks(withMediaType: .video).first else {
|
||||
print("Failed to get video track")
|
||||
throw AVError(.decodeFailed)
|
||||
}
|
||||
let size = track.naturalSize.applying(track.preferredTransform)
|
||||
let width = Int(abs(size.width))
|
||||
let height = Int(abs(size.height))
|
||||
let fps = track.nominalFrameRate
|
||||
let duration = CMTimeGetSeconds(asset.duration)
|
||||
let totalFrames = Int(fps * Float(duration))
|
||||
let animDur = Double(totalFrames) / Double(fps)
|
||||
var camlData = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<caml xmlns="http://www.apple.com/CoreAnimation/1.0">
|
||||
<CALayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" bounds="0 0 \(width) \(height)" contentsFormat="RGBA8" cornerCurve="circular" hidden="0" name="_FLOATING" position="\(Int(width/2)) \(Int(height/2))">
|
||||
<sublayers>
|
||||
<CATransformLayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" allowsHitTesting="1" bounds="0 0 \(width) \(height)" contentsFormat="RGBA8" cornerCurve="circular" name="Chip" position="\(Int(width/2)) \(Int(height/2))">
|
||||
<sublayers>
|
||||
<CALayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" bounds="0 0 \(width) \(height)" contentsFormat="RGBA8" cornerCurve="circular" name="CALayer1" position="\(Int(width/2)) \(Int(height/2))">
|
||||
<contents type="CGImage" src="assets/0.jpg"/>
|
||||
<animations>
|
||||
<animation type="CAKeyframeAnimation" calculationMode="linear" keyPath="contents" beginTime="1e-100" duration="\(animDur)" removedOnCompletion="0" repeatCount="inf" repeatDuration="0" speed="1" timeOffset="0" autoreverses="\(autoReverses ? 1 : 0)">
|
||||
<values>\n
|
||||
"""
|
||||
|
||||
// go through every frame and add it to the caml
|
||||
do {
|
||||
let reader = try AVAssetReader(asset: asset)
|
||||
let readerOutputSettings: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)
|
||||
]
|
||||
|
||||
let readerOutput = AVAssetReaderTrackOutput(track: track, outputSettings: readerOutputSettings)
|
||||
reader.add(readerOutput)
|
||||
|
||||
reader.startReading()
|
||||
|
||||
var frameCount = 0
|
||||
|
||||
while let sampleBuffer = readerOutput.copyNextSampleBuffer(),
|
||||
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
|
||||
try? autoreleasepool {
|
||||
let name = "\(frameCount).jpg"
|
||||
let filePathName = "assets/\(name)"
|
||||
UIApplication.shared.change(title: NSLocalizedString("Generating Video...", comment: ""), body: String(format: NSLocalizedString("Creating %@...", comment: "the message for the current image that is being generated"), filePathName))
|
||||
|
||||
let ciImage = CIImage(cvPixelBuffer: imageBuffer)
|
||||
let context = CIContext()
|
||||
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
|
||||
if let img = cgImage.jpg {
|
||||
try img.write(to: assetsFolder.appendingPathComponent(name))
|
||||
camlData += "\t\t\t<CGImage src=\"assets/\(name)\"/>\n"
|
||||
frameCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("Error reading asset: \(error)")
|
||||
}
|
||||
UIApplication.shared.change(title: NSLocalizedString("Generating Video...", comment: ""), body: NSLocalizedString("Creating CAML data...", comment: "translate this however you see fits"))
|
||||
|
||||
camlData += """
|
||||
</values>
|
||||
</animation>
|
||||
</animations>
|
||||
</CALayer>
|
||||
</sublayers>
|
||||
</CATransformLayer>
|
||||
</sublayers>
|
||||
<states>
|
||||
<LKState name="Locked">
|
||||
<elements/>
|
||||
</LKState>
|
||||
<LKState name="Unlock">
|
||||
<elements/>
|
||||
</LKState>
|
||||
<LKState name="Sleep">
|
||||
<elements/>
|
||||
</LKState>
|
||||
</states>
|
||||
<stateTransitions>
|
||||
<LKStateTransition fromState="*" toState="Unlock">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="Unlock" toState="*">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="*" toState="Locked">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="Locked" toState="*">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="*" toState="Sleep">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="Sleep" toState="*">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
</stateTransitions>
|
||||
</CALayer>
|
||||
</caml>
|
||||
"""
|
||||
// write the caml
|
||||
guard let caData = camlData.data(using: .utf8) else { throw URLError(.unknown) }
|
||||
try caData.write(to: coreAnimDir.appendingPathComponent("main.caml"))
|
||||
|
||||
// write the other info xml
|
||||
camlData = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>assetManifest</key>
|
||||
<string>assetManifest.caml</string>
|
||||
<key>documentHeight</key>
|
||||
<real>\(height)</real>
|
||||
<key>documentResizesToView</key>
|
||||
<true/>
|
||||
<key>documentWidth</key>
|
||||
<real>\(width)</real>
|
||||
<key>dynamicGuidesEnabled</key>
|
||||
<true/>
|
||||
<key>geometryFlipped</key>
|
||||
<false/>
|
||||
<key>guidesEnabled</key>
|
||||
<true/>
|
||||
<key>interactiveMouseEventsEnabled</key>
|
||||
<true/>
|
||||
<key>interactiveShowsCursor</key>
|
||||
<true/>
|
||||
<key>interactiveTouchEventsEnabled</key>
|
||||
<false/>
|
||||
<key>loopEnd</key>
|
||||
<real>0.0</real>
|
||||
<key>loopStart</key>
|
||||
<real>0.0</real>
|
||||
<key>loopingEnabled</key>
|
||||
<false/>
|
||||
<key>multitouchDisablesMouse</key>
|
||||
<false/>
|
||||
<key>multitouchEnabled</key>
|
||||
<false/>
|
||||
<key>presentationMouseEventsEnabled</key>
|
||||
<true/>
|
||||
<key>presentationShowsCursor</key>
|
||||
<true/>
|
||||
<key>presentationTouchEventsEnabled</key>
|
||||
<false/>
|
||||
<key>rootDocument</key>
|
||||
<string>main.caml</string>
|
||||
<key>savesWindowFrame</key>
|
||||
<false/>
|
||||
<key>scalesToFitInPlayer</key>
|
||||
<true/>
|
||||
<key>showsTouches</key>
|
||||
<true/>
|
||||
<key>snappingEnabled</key>
|
||||
<true/>
|
||||
<key>timelineMarkers</key>
|
||||
<string>[(null)]</string>
|
||||
<key>touchesColor</key>
|
||||
<string>1 1 0 0.8</string>
|
||||
<key>unitsInPixelsInPlayer</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
|
||||
guard let xmlData = camlData.data(using: .utf8) else { throw URLError(.unknown) }
|
||||
try xmlData.write(to: coreAnimDir.appendingPathComponent("index.xml", conformingTo: .xml))
|
||||
|
||||
return descrURL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// ApplyError.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/18/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ApplyError: LocalizedError {
|
||||
// Throw when the app hash is incorrect
|
||||
case wrongAppHash
|
||||
|
||||
// Throw when the collections need to be reset
|
||||
case collectionsNeedsReset
|
||||
|
||||
// Throw in all other cases
|
||||
case unexpected(info: String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .wrongAppHash:
|
||||
return NSLocalizedString("Your app hash is incorrect. Please set it again.", comment: "")
|
||||
case .collectionsNeedsReset:
|
||||
return NSLocalizedString("The folder is improperly set up. Please tap \"Reset Collections\" and try again.", comment: "")
|
||||
case .unexpected(let info):
|
||||
return info
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import UIKit
|
||||
|
||||
// credit: sourcelocation & TrollTools
|
||||
var currentUIAlertController: UIAlertController?
|
||||
var lastDismissTitle: String?
|
||||
|
||||
|
||||
fileprivate let errorString = NSLocalizedString("Error", comment: "")
|
||||
@@ -19,6 +20,7 @@ extension UIApplication {
|
||||
|
||||
func dismissAlert(animated: Bool) {
|
||||
DispatchQueue.main.async {
|
||||
lastDismissTitle = currentUIAlertController?.title
|
||||
currentUIAlertController?.dismiss(animated: animated)
|
||||
}
|
||||
}
|
||||
@@ -32,7 +34,7 @@ extension UIApplication {
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
||||
let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
|
||||
let systemVersion = device.systemVersion
|
||||
body += "\n\(device.systemName) \(systemVersion), version \(appVersion) build \(appBuild)"
|
||||
body += "\n\(device.systemName) \(systemVersion), v\(appVersion) build \(appBuild)"
|
||||
}
|
||||
|
||||
currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
|
||||
@@ -60,8 +62,8 @@ extension UIApplication {
|
||||
}
|
||||
|
||||
func present(alert: UIAlertController) {
|
||||
if var topController = self.windows[0].rootViewController {
|
||||
while let presentedViewController = topController.presentedViewController {
|
||||
if let window = UIApplication.shared.connectedScenes.map({ $0 as? UIWindowScene }).compactMap({ $0 }).first?.windows.first, var topController = window.rootViewController {
|
||||
while let presentedViewController = topController.presentedViewController, presentedViewController.title != lastDismissTitle {
|
||||
topController = presentedViewController
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// LoopingPlayerUIView.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/11/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
class LoopingPlayerUIView: UIView {
|
||||
|
||||
private var playerLayer = AVPlayerLayer()
|
||||
private var playerLooper: AVPlayerLooper?
|
||||
private var player = AVQueuePlayer()
|
||||
|
||||
init(videoURL: URL){
|
||||
let asset = AVAsset(url: videoURL)
|
||||
let item = AVPlayerItem(asset: asset)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
// Setup the player
|
||||
playerLayer.player = player
|
||||
playerLayer.videoGravity = .resizeAspectFill
|
||||
layer.addSublayer(playerLayer)
|
||||
|
||||
// Create a new player looper with the queue player and template item
|
||||
playerLooper = AVPlayerLooper(player: player, templateItem: item)
|
||||
player.play()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
playerLayer.frame = bounds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// PlayerView.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/11/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PlayerView: UIViewRepresentable {
|
||||
var videoURL: URL
|
||||
|
||||
init(videoURL: URL) {
|
||||
self.videoURL = videoURL
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
return LoopingPlayerUIView(videoURL: videoURL)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,262 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"CFBundleName" : {
|
||||
"comment" : "Bundle name",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"ar-AE" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster Ar"
|
||||
}
|
||||
},
|
||||
"ar-SA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"cs" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"fr-BE" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"hu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Nugget Wallpaper" : {
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "خلفيات Nugget"
|
||||
}
|
||||
},
|
||||
"ar-AE" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "خلفيات Nugget"
|
||||
}
|
||||
},
|
||||
"ar-SA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "خلفيات Nugget"
|
||||
}
|
||||
},
|
||||
"cs" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Tapeta"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Hintergrundbilder"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Wallpaper"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Wallpaper"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Fond d'écran Nugget"
|
||||
}
|
||||
},
|
||||
"fr-BE" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Fond écran nugget"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Fond d'écran Nugget"
|
||||
}
|
||||
},
|
||||
"hu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Háttérkép"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Wallpaper"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget 배경화면"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Achtergrond"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tapeta Nugget"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Обои Nugget"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "วอลเปเปอร์ Nugget"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Duvar Kağıdı"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Hình nền Nugget"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget 壁紙"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -13,41 +13,32 @@ struct Pocket_PosterApp: App {
|
||||
@AppStorage("finishedTutorial") var finishedTutorial: Bool = false
|
||||
@AppStorage("pbHash") var pbHash: String = ""
|
||||
|
||||
@State var selectedTendies: [URL] = []
|
||||
|
||||
@State var downloadURL: String? = nil
|
||||
@State var showDownloadAlert = false
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
Group {
|
||||
if finishedTutorial {
|
||||
ContentView(selectedTendies: $selectedTendies)
|
||||
RootView()
|
||||
} else {
|
||||
OnBoardingView(cards: onBoardingCards, isFinished: $finishedTutorial)
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
.animation(.easeOut(duration: 0.5), value: finishedTutorial)
|
||||
.alert("Download Tendies File", isPresented: $showDownloadAlert) {
|
||||
Button("OK") {
|
||||
downloadWallpaper()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text("Would you like to download the file \(DownloadManager.getWallpaperNameFromURL(string: downloadURL ?? "/Unknown"))?")
|
||||
}
|
||||
.onOpenURL(perform: { url in
|
||||
// Download URL
|
||||
if url.absoluteString.starts(with: "pocketposter://download") {
|
||||
// prohibit to only tendies files
|
||||
if !url.absoluteString.hasSuffix(".tendies") {
|
||||
UIApplication.shared.alert(body: "Only .tendies files can be downloaded!")
|
||||
} else if selectedTendies.count >= PosterBoardManager.MaxTendies {
|
||||
UIApplication.shared.alert(title: "Max Tendies Reached", body: "You can only apply \(PosterBoardManager.MaxTendies) descriptors.")
|
||||
UIApplication.shared.alert(body: NSLocalizedString("Only .tendies files can be downloaded!", comment: ""))
|
||||
} else if PosterBoardManager.shared.selectedTendies.count >= PosterBoardManager.MaxTendies {
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Max Tendies Reached", comment: ""), body: String(format: NSLocalizedString("You can only apply %@ descriptors.", comment: ""), "\(PosterBoardManager.MaxTendies)"))
|
||||
} else {
|
||||
downloadURL = url.absoluteString.replacingOccurrences(of: "pocketposter://download?url=", with: "")
|
||||
showDownloadAlert = true
|
||||
UIApplication.shared.confirmAlert(title: NSLocalizedString("Download Tendies File", comment: ""), body: String(format: NSLocalizedString("Would you like to download the file %@?", comment: ""), "\(DownloadManager.getWallpaperNameFromURL(string: downloadURL ?? "/Unknown"))"), onOK: {
|
||||
downloadWallpaper()
|
||||
}, noCancel: false)
|
||||
}
|
||||
}
|
||||
// App Hash URL
|
||||
@@ -55,18 +46,18 @@ struct Pocket_PosterApp: App {
|
||||
pbHash = url.absoluteString.replacingOccurrences(of: "pocketposter://app-hash?uuid=", with: "")
|
||||
}
|
||||
else if url.pathExtension == "tendies" {
|
||||
if selectedTendies.count >= PosterBoardManager.MaxTendies {
|
||||
UIApplication.shared.alert(title: "Max Tendies Reached", body: "You can only apply \(PosterBoardManager.MaxTendies) descriptors.")
|
||||
if PosterBoardManager.shared.selectedTendies.count >= PosterBoardManager.MaxTendies {
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Max Tendies Reached", comment: ""), body: String(format: NSLocalizedString("You can only apply %@ descriptors.", comment: ""), "\(PosterBoardManager.MaxTendies)"))
|
||||
} else {
|
||||
// copy it over to the KFC bucket
|
||||
do {
|
||||
let newURL = try DownloadManager.copyTendies(from: url)
|
||||
selectedTendies.append(newURL)
|
||||
PosterBoardManager.shared.selectedTendies.append(newURL)
|
||||
Haptic.shared.notify(.success)
|
||||
UIApplication.shared.alert(title: "Successfully imported \(url.lastPathComponent)", body: "")
|
||||
UIApplication.shared.alert(title: String(format: NSLocalizedString("Successfully imported %@", comment: ""), "\(url.lastPathComponent)"), body: "")
|
||||
} catch {
|
||||
Haptic.shared.notify(.error)
|
||||
UIApplication.shared.alert(title: "Failed to import tendies", body: error.localizedDescription)
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Failed to import tendies", comment: ""), body: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +72,7 @@ struct Pocket_PosterApp: App {
|
||||
Task {
|
||||
do {
|
||||
let newURL = try await DownloadManager.downloadFromURL(string: downloadURL!)
|
||||
selectedTendies.append(newURL)
|
||||
PosterBoardManager.shared.selectedTendies.append(newURL)
|
||||
Haptic.shared.notify(.success)
|
||||
UIApplication.shared.dismissAlert(animated: true)
|
||||
} catch {
|
||||
@@ -91,4 +82,9 @@ struct Pocket_PosterApp: App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
// clear the videos cache
|
||||
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent("Videos", conformingTo: .directory))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
9183
|
||||
@@ -0,0 +1 @@
|
||||
PRPosterRoleLockScreen
|
||||
Binary file not shown.
+58
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>assetManifest</key>
|
||||
<string>assetManifest.caml</string>
|
||||
<key>documentHeight</key>
|
||||
<real>3176</real>
|
||||
<key>documentResizesToView</key>
|
||||
<false/>
|
||||
<key>documentWidth</key>
|
||||
<real>3176</real>
|
||||
<key>dynamicGuidesEnabled</key>
|
||||
<true/>
|
||||
<key>geometryFlipped</key>
|
||||
<false/>
|
||||
<key>guidesEnabled</key>
|
||||
<true/>
|
||||
<key>interactiveMouseEventsEnabled</key>
|
||||
<true/>
|
||||
<key>interactiveShowsCursor</key>
|
||||
<true/>
|
||||
<key>interactiveTouchEventsEnabled</key>
|
||||
<false/>
|
||||
<key>loopEnd</key>
|
||||
<real>0.0</real>
|
||||
<key>loopStart</key>
|
||||
<real>0.0</real>
|
||||
<key>loopingEnabled</key>
|
||||
<false/>
|
||||
<key>multitouchDisablesMouse</key>
|
||||
<false/>
|
||||
<key>multitouchEnabled</key>
|
||||
<false/>
|
||||
<key>presentationMouseEventsEnabled</key>
|
||||
<true/>
|
||||
<key>presentationShowsCursor</key>
|
||||
<true/>
|
||||
<key>presentationTouchEventsEnabled</key>
|
||||
<false/>
|
||||
<key>rootDocument</key>
|
||||
<string>main.caml</string>
|
||||
<key>savesWindowFrame</key>
|
||||
<false/>
|
||||
<key>scalesToFitInPlayer</key>
|
||||
<false/>
|
||||
<key>showsTouches</key>
|
||||
<true/>
|
||||
<key>snappingEnabled</key>
|
||||
<true/>
|
||||
<key>timelineMarkers</key>
|
||||
<string>[(null)]</string>
|
||||
<key>touchesColor</key>
|
||||
<string>1 1 0 0.8</string>
|
||||
<key>unitsInPixelsInPlayer</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<caml xmlns="http://www.apple.com/CoreAnimation/1.0">
|
||||
<CALayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" bounds="0 0 3176 3176" contentsFormat="RGBA8" cornerCurve="circular" hidden="0" name="_BACKGROUND" position="1588 1588">
|
||||
<sublayers>
|
||||
<CALayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" anchorPoint="0 0" bounds="0 0 0 0" contentsFormat="RGBA8" cornerCurve="circular" name="_CENTER_BACKGROUND" position="1588 1588"/>
|
||||
</sublayers>
|
||||
<states>
|
||||
<LKState name="Locked">
|
||||
<elements/>
|
||||
</LKState>
|
||||
<LKState name="Unlock">
|
||||
<elements/>
|
||||
</LKState>
|
||||
<LKState name="Sleep">
|
||||
<elements/>
|
||||
</LKState>
|
||||
</states>
|
||||
<stateTransitions>
|
||||
<LKStateTransition fromState="*" toState="Unlock">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="Unlock" toState="*">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="*" toState="Locked">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="Locked" toState="*">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="*" toState="Sleep">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
<LKStateTransition fromState="Sleep" toState="*">
|
||||
<elements/>
|
||||
</LKStateTransition>
|
||||
</stateTransitions>
|
||||
</CALayer>
|
||||
</caml>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>appearanceAware</key>
|
||||
<true/>
|
||||
<key>assets</key>
|
||||
<dict>
|
||||
<key>lockAndHome</key>
|
||||
<dict>
|
||||
<key>default</key>
|
||||
<dict>
|
||||
<key>backgroundAnimationFileName</key>
|
||||
<string>9183.Custom_Background-810w-1080h@2x~ipad.ca</string>
|
||||
<key>floatingAnimationFileNameKey</key>
|
||||
<string>9183.Custom_Floating-810w-1080h@2x~ipad.ca</string>
|
||||
<key>identifier</key>
|
||||
<integer>9183</integer>
|
||||
<key>name</key>
|
||||
<string>Chip</string>
|
||||
<key>type</key>
|
||||
<string>LayeredAnimation</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>contentVersion</key>
|
||||
<real>2.01</real>
|
||||
<key>family</key>
|
||||
<string>Chip</string>
|
||||
<key>identifier</key>
|
||||
<integer>9183</integer>
|
||||
<key>logicalScreenClass</key>
|
||||
<string>810w-1080h@2x~ipad</string>
|
||||
<key>name</key>
|
||||
<string>Chip</string>
|
||||
<key>preferredProminentColor</key>
|
||||
<dict>
|
||||
<key>dark</key>
|
||||
<string>#00000</string>
|
||||
<key>default</key>
|
||||
<string>#FFFFFF</string>
|
||||
</dict>
|
||||
<key>version</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>posterEnvironmentOverrides</key>
|
||||
<data>
|
||||
e30=
|
||||
</data>
|
||||
<key>wallpaperRepresentingFileName</key>
|
||||
<string>9183.Custom-810w-1080h@2x~ipad.wallpaper</string>
|
||||
<key>wallpaperRepresentingIdentifier</key>
|
||||
<string>9999</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import PhotosUI
|
||||
|
||||
extension UIDocumentPickerViewController {
|
||||
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
|
||||
@@ -18,13 +19,11 @@ struct ContentView: View {
|
||||
// Prefs
|
||||
@AppStorage("pbHash") var pbHash: String = ""
|
||||
|
||||
@ObservedObject var pbManager = PosterBoardManager.shared
|
||||
|
||||
private let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
|
||||
|
||||
@State var showTendiesImporter: Bool = false
|
||||
var selectedTendies: Binding<[URL]>
|
||||
|
||||
@State var showErrorAlert = false
|
||||
@State var lastError: String?
|
||||
@State var hideResetHelp: Bool = true
|
||||
|
||||
var body: some View {
|
||||
@@ -40,16 +39,16 @@ struct ContentView: View {
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
showTendiesImporter.toggle()
|
||||
}) {
|
||||
Text("Select Tendies")
|
||||
Label("Select Tendies", systemImage: "document.circle")
|
||||
}
|
||||
.buttonStyle(TintedButton(color: .green, fullwidth: true))
|
||||
}
|
||||
.listRowInsets(EdgeInsets())
|
||||
.padding(7)
|
||||
|
||||
if !selectedTendies.wrappedValue.isEmpty {
|
||||
if !pbManager.selectedTendies.isEmpty {
|
||||
Section {
|
||||
ForEach(selectedTendies.wrappedValue, id: \.self) { tendie in
|
||||
ForEach(pbManager.selectedTendies, id: \.self) { tendie in
|
||||
Text(tendie.deletingPathExtension().lastPathComponent)
|
||||
}
|
||||
.onDelete(perform: delete)
|
||||
@@ -63,42 +62,56 @@ struct ContentView: View {
|
||||
Text("Enter your PosterBoard app hash in Settings.")
|
||||
} else {
|
||||
VStack {
|
||||
if !selectedTendies.wrappedValue.isEmpty {
|
||||
if !pbManager.selectedTendies.isEmpty || !pbManager.videos.isEmpty {
|
||||
Button(action: {
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Applying Tendies...", comment: ""), body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
try PosterBoardManager.applyTendies(selectedTendies.wrappedValue, appHash: pbHash)
|
||||
selectedTendies.wrappedValue.removeAll()
|
||||
try pbManager.applyTendies(appHash: pbHash)
|
||||
SymHandler.cleanup() // just to be extra sure
|
||||
try? FileManager.default.removeItem(at: PosterBoardManager.getTendiesStoreURL())
|
||||
Haptic.shared.notify(.success)
|
||||
try? FileManager.default.removeItem(at: pbManager.getTendiesStoreURL())
|
||||
UIApplication.shared.dismissAlert(animated: false)
|
||||
UIApplication.shared.confirmAlert(title: "Success!", body: "The PosterBoard app will now open. Please close it from the app switcher.", onOK: {
|
||||
if !PosterBoardManager.openPosterBoard() {
|
||||
UIApplication.shared.confirmAlert(title: "Falling Back to Shortcut", body: "PosterBoard failed to open directly. The fallback shortcut will now be opened.", onOK: {
|
||||
PosterBoardManager.runShortcut(named: "PosterBoard")
|
||||
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 {
|
||||
Haptic.shared.notify(.error)
|
||||
SymHandler.cleanup()
|
||||
UIApplication.shared.dismissAlert(animated: false)
|
||||
UIApplication.shared.alert(body: error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
presentError(ApplyError.unexpected(info: error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text("Apply")
|
||||
Label("Apply", systemImage: "checkmark.circle")
|
||||
}
|
||||
.buttonStyle(TintedButton(color: .blue, fullwidth: true))
|
||||
}
|
||||
Button(action: {
|
||||
hideResetHelp = false
|
||||
guard let lang = UserDefaults.standard.stringArray(forKey: "AppleLanguages")?.first else {
|
||||
hideResetHelp = false // fallback to tutorial
|
||||
return
|
||||
}
|
||||
UIApplication.shared.confirmAlert(title: NSLocalizedString("Reset Collections", comment: ""), body: NSLocalizedString("Do you want to reset collections?", comment: ""), onOK: {
|
||||
if pbManager.setSystemLanguage(to: lang) {
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Collections Successfully Reset!", comment: ""), body: NSLocalizedString("Your PosterBoard will refresh automatically.", comment: ""))
|
||||
} else {
|
||||
UIApplication.shared.alert(body: "The API failed to call correctly.\nSystem Locale Code: \(lang)")
|
||||
}
|
||||
}, noCancel: false)
|
||||
}) {
|
||||
Text("Reset Collections")
|
||||
Label("Reset Collections", systemImage: "arrow.clockwise.circle")
|
||||
}
|
||||
.buttonStyle(TintedButton(color: .red, fullwidth: true))
|
||||
}
|
||||
@@ -130,21 +143,16 @@ struct ContentView: View {
|
||||
.fileImporter(isPresented: $showTendiesImporter, allowedContentTypes: [UTType(filenameExtension: "tendies", conformingTo: .data)!], allowsMultipleSelection: true, onCompletion: { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
if selectedTendies.wrappedValue.count + url.count > PosterBoardManager.MaxTendies {
|
||||
UIApplication.shared.alert(title: "Max Tendies Reached", body: "You can only apply \(PosterBoardManager.MaxTendies) descriptors.")
|
||||
if pbManager.selectedTendies.count + url.count > PosterBoardManager.MaxTendies {
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Max Tendies Reached", comment: ""), body: String(format: NSLocalizedString("You can only apply %@ descriptors.", comment: ""), "\(PosterBoardManager.MaxTendies)"))
|
||||
} else {
|
||||
selectedTendies.wrappedValue.append(contentsOf: url)
|
||||
pbManager.selectedTendies.append(contentsOf: url)
|
||||
}
|
||||
case .failure(let error):
|
||||
lastError = error.localizedDescription
|
||||
showErrorAlert.toggle()
|
||||
Haptic.shared.notify(.error)
|
||||
UIApplication.shared.alert(body: error.localizedDescription)
|
||||
}
|
||||
})
|
||||
.alert("Error", isPresented: $showErrorAlert) {
|
||||
Button("OK") {}
|
||||
} message: {
|
||||
Text(lastError ?? "???")
|
||||
}
|
||||
.overlay {
|
||||
OnBoardingView(cards: resetCollectionsInfo, isFinished: $hideResetHelp)
|
||||
.opacity(hideResetHelp ? 0.0 : 1.0)
|
||||
@@ -154,11 +162,19 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func delete(at offsets: IndexSet) {
|
||||
selectedTendies.wrappedValue.remove(atOffsets: offsets)
|
||||
pbManager.selectedTendies.remove(atOffsets: offsets)
|
||||
}
|
||||
|
||||
init(selectedTendies: Binding<[URL]>) {
|
||||
self.selectedTendies = selectedTendies
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
init() {
|
||||
// Fix file picker
|
||||
let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))!
|
||||
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))!
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
//
|
||||
// VideoView.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/11/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import AVKit
|
||||
|
||||
struct VideoView: View {
|
||||
@ObservedObject var pbManager = PosterBoardManager.shared
|
||||
@AppStorage("ignoreDurationLimit") var ignoreDurationLimit: Bool = false
|
||||
|
||||
@State var selectedVideo: PhotosPickerItem?
|
||||
@State private var currentPage: Int = 0
|
||||
|
||||
@State private var currentDate = Date()
|
||||
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() // update every second
|
||||
private var dateFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE, MMMM d"
|
||||
return formatter
|
||||
}
|
||||
private var timeFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
if Locale.current.hourCycle == .zeroToEleven || Locale.current.hourCycle == .oneToTwelve {
|
||||
formatter.dateFormat = "h:mm"
|
||||
} else {
|
||||
formatter.dateFormat = "HH:mm"
|
||||
}
|
||||
return formatter
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
GeometryReader { geom in
|
||||
TabView(selection: $currentPage) {
|
||||
ForEach(Array(pbManager.videos.enumerated()), id: \.offset) { idx, vid in
|
||||
ZStack {
|
||||
switch vid.loadState {
|
||||
case .unknown:
|
||||
EmptyView()
|
||||
case .loading:
|
||||
ProgressView()
|
||||
case .loaded(let movie):
|
||||
PlayerView(videoURL: movie.url)
|
||||
.scaledToFill()
|
||||
.frame(width: geom.size.width * 0.7, height: geom.size.height * 0.8)
|
||||
.clipped()
|
||||
VStack {
|
||||
Text(dateFormatter.string(from: currentDate))
|
||||
.shadow(radius: 10)
|
||||
|
||||
Text(timeFormatter.string(from: currentDate))
|
||||
.font(.system(size: 75, weight: .semibold, design: .rounded))
|
||||
.shadow(radius: 10)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 45)
|
||||
.foregroundStyle(.white)
|
||||
case .failed:
|
||||
Text("Failed")
|
||||
}
|
||||
}
|
||||
.frame(width: geom.size.width * 0.7, height: geom.size.height * 0.8)
|
||||
.shadow(radius: 5)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.cornerRadius(24)
|
||||
.tag(idx)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.stroke(.gray, lineWidth: 4)
|
||||
}
|
||||
.overlay(
|
||||
// Delete Button
|
||||
DeleteButton(video: vid, onDelete: removeVideo),
|
||||
alignment: .topLeading
|
||||
)
|
||||
.overlay(
|
||||
// Loops Button
|
||||
Button(action: {
|
||||
pbManager.videos[idx].autoReverses = !vid.autoReverses
|
||||
}) {
|
||||
Image(systemName: "arrow.left.arrow.right.circle")
|
||||
.font(.title)
|
||||
.foregroundStyle(Color(uiColor: .label))
|
||||
.background {
|
||||
if vid.autoReverses {
|
||||
Circle()
|
||||
.foregroundStyle(.blue.opacity(0.8))
|
||||
.shadow(radius: 5)
|
||||
} else {
|
||||
Circle()
|
||||
.foregroundStyle(.regularMaterial)
|
||||
.shadow(radius: 5)
|
||||
}
|
||||
}
|
||||
}.offset(x: 8, y: -8),
|
||||
alignment: .topTrailing
|
||||
)
|
||||
}
|
||||
.onDelete(perform: removeVideo)
|
||||
.onReceive(timer) { input in
|
||||
currentDate = input
|
||||
}
|
||||
|
||||
// MARK: Select Photo Option
|
||||
if selectedVideo == nil && pbManager.videos.count < 5 {
|
||||
ZStack {
|
||||
PhotosPicker(selection: $selectedVideo, matching: .videos, label: {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.fill(.gray.opacity(0.4))
|
||||
|
||||
Circle()
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundStyle(.blue)
|
||||
|
||||
Image(systemName: "plus")
|
||||
.font(.title)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
})
|
||||
}
|
||||
.frame(width: geom.size.width * 0.7, height: geom.size.height * 0.8)
|
||||
.shadow(radius: 5)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.cornerRadius(24)
|
||||
.tag(pbManager.videos.count)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.stroke(.gray, lineWidth: 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedVideo) { _ in
|
||||
if selectedVideo == nil { return }
|
||||
let id = pbManager.videos.count
|
||||
pbManager.videos.append(.init(loadState: .loading))
|
||||
Task {
|
||||
do {
|
||||
if let movie = try await selectedVideo?.loadTransferable(type: Movie.self) {
|
||||
await MainActor.run {
|
||||
if !ignoreDurationLimit && VideoHandler.isVideoTooLong(at: movie.url) {
|
||||
pbManager.videos.remove(at: id)
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Failed to Import Video", comment: ""), body: String(format: NSLocalizedString("The video you imported is too long! Your video must be %@ seconds or less.", comment: ""), "\(Int(VideoHandler.MaxDurationSecs))"))
|
||||
} else {
|
||||
pbManager.videos[id].loadState = .loaded(movie)
|
||||
currentPage = id
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await MainActor.run {
|
||||
pbManager.videos[id].loadState = .failed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
pbManager.videos[id].loadState = .failed
|
||||
}
|
||||
}
|
||||
selectedVideo = nil
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeVideo(at offsets: IndexSet) {
|
||||
withAnimation {
|
||||
pbManager.videos.remove(atOffsets: offsets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DeleteButton: View {
|
||||
@Environment(\.editMode) var editMode
|
||||
|
||||
let video: LoadInfo
|
||||
@ObservedObject var pbManager = PosterBoardManager.shared
|
||||
let onDelete: (IndexSet) -> ()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Button(action: {
|
||||
if let index = pbManager.videos.firstIndex(of: video) {
|
||||
self.onDelete(IndexSet(integer: index))
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "minus.circle")
|
||||
.foregroundStyle(.red)
|
||||
.font(.title)
|
||||
.background {
|
||||
Circle()
|
||||
.foregroundStyle(.regularMaterial)
|
||||
.shadow(radius: 5)
|
||||
}
|
||||
}
|
||||
.offset(x: -8, y: -8)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,30 +29,30 @@ struct OnBoardingPage: Identifiable {
|
||||
|
||||
let onBoardingCards: [OnBoardingPage] = [
|
||||
.init(
|
||||
title: "Welcome to Pocket Poster!",
|
||||
description: "Here is a tutorial to help you get started with the app.",
|
||||
title: NSLocalizedString("Welcome to Pocket Poster!", comment: ""),
|
||||
description: NSLocalizedString("Here is a tutorial to help you get started with the app.", comment: ""),
|
||||
image: "Logo"
|
||||
),
|
||||
.init(
|
||||
title: "Install the Fallback Shortcut (Optional)",
|
||||
description: "To apply, PosterBoard will need to open.\n\nYou can install an optional shortcut if the original method fails.",
|
||||
title: NSLocalizedString("Install the Fallback Shortcut (Optional)", comment: ""),
|
||||
description: NSLocalizedString("To apply, PosterBoard will need to open.", comment: "") + "\n\n" + NSLocalizedString("You can install an optional shortcut if the original method fails.", comment: ""),
|
||||
image: "Shortcuts",
|
||||
link: URL(string: PosterBoardManager.ShortcutURL),
|
||||
linkName: "Get Shortcut"
|
||||
linkName: NSLocalizedString("Get Shortcut", comment: "")
|
||||
),
|
||||
.init(
|
||||
title: "Install Nugget",
|
||||
description: "To get the app bundle id, Nugget is required.\n\nOn your computer, download Nugget from the GitHub.",
|
||||
title: NSLocalizedString("Install Nugget", comment: ""),
|
||||
description: NSLocalizedString("To get the app bundle id, Nugget is required.", comment: "") + "\n\n" + NSLocalizedString("On your computer, download Nugget from the GitHub.", comment: ""),
|
||||
image: "Nugget",
|
||||
link: URL(string: "https://github.com/leminlimez/Nugget/releases/latest"),
|
||||
linkName: "Open GitHub"
|
||||
linkName: NSLocalizedString("Open GitHub", comment: "")
|
||||
),
|
||||
.init(
|
||||
title: "Enjoy!",
|
||||
description: "You can find wallpapers on the official Cowabun.ga website.",
|
||||
title: NSLocalizedString("Enjoy!", comment: ""),
|
||||
description: NSLocalizedString("You can find wallpapers on the official Cowabun.ga website.", comment: ""),
|
||||
image: "Cowabunga",
|
||||
link: URL(string: PosterBoardManager.WallpapersURL),
|
||||
linkName: "Find Wallpapers"
|
||||
linkName: NSLocalizedString("Find Wallpapers", comment: "")
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// RootView.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/11/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RootView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
ContentView()
|
||||
.tabItem {
|
||||
Label("Home", systemImage: "house")
|
||||
}
|
||||
VideoView()
|
||||
.tabItem {
|
||||
Label("Videos", systemImage: "camera")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,13 @@ import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
// Prefs
|
||||
@AppStorage("pbHash") var pbHash: String = ""
|
||||
@AppStorage("pbHash") var pbHash: String = "" // PosterBoard hash
|
||||
@AppStorage("cpHash") var cpHash: String = "" // CarPlay hash
|
||||
@AppStorage("ignoreDurationLimit") var ignoreDurationLimit: Bool = false
|
||||
|
||||
@State var checkingForHash: Bool = false
|
||||
@State var hashCheckTask: Task<Void, any Error>? = nil
|
||||
|
||||
@State var showErrorAlert = false
|
||||
@State var errorAlertTitle: String?
|
||||
@State var errorAlertDescr: String?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
@@ -49,12 +47,20 @@ struct SettingsView: View {
|
||||
Label("App Hash", systemImage: "lock.app.dashed")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $ignoreDurationLimit, label: {
|
||||
Label("Disable Video Duration Limit", systemImage: "ruler")
|
||||
})
|
||||
} header: {
|
||||
Label("Preferences", systemImage: "gear")
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(action: {
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
UserDefaults.standard.set(false, forKey: "finishedTutorial")
|
||||
}) {
|
||||
Text("Replay Tutorial")
|
||||
Label("Replay Tutorial", systemImage: "questionmark.circle")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
@@ -62,17 +68,13 @@ struct SettingsView: View {
|
||||
do {
|
||||
try PosterBoardManager.clearCache()
|
||||
Haptic.shared.notify(.success)
|
||||
errorAlertTitle = "App Cache Successfully Cleared!"
|
||||
errorAlertDescr = ""
|
||||
showErrorAlert = true
|
||||
UIApplication.shared.alert(title: NSLocalizedString("App Cache Successfully Cleared!", comment: ""), body: "")
|
||||
} catch {
|
||||
Haptic.shared.notify(.error)
|
||||
errorAlertTitle = "Error"
|
||||
errorAlertDescr = error.localizedDescription
|
||||
showErrorAlert = true
|
||||
UIApplication.shared.alert(body: error.localizedDescription)
|
||||
}
|
||||
}) {
|
||||
Text("Clear App Cache")
|
||||
Label("Clear App Cache", systemImage: "trash.circle")
|
||||
}
|
||||
.foregroundStyle(.red)
|
||||
} header: {
|
||||
@@ -118,19 +120,15 @@ struct SettingsView: View {
|
||||
// MARK: Credits
|
||||
Section {
|
||||
LinkCell(imageName: "leminlimez", url: "https://github.com/leminlimez", title: "LeminLimez", contribution: NSLocalizedString("Main Developer", comment: "leminlimez's contribution"), circle: true)
|
||||
LinkCell(imageName: "serstars", url: "https://github.com/SerStars", title: "SerStars", contribution: "Website Designer", circle: true)
|
||||
LinkCell(imageName: "Nathan", url: "https://github.com/verygenericname", title: "Nathan", contribution: "Exploit", circle: true)
|
||||
LinkCell(imageName: "duy", url: "https://github.com/khanhduytran0", title: "DuyKhanhTran", contribution: "Exploit", circle: true)
|
||||
LinkCell(imageName: "sky", url: "https://bsky.app/profile/did:plc:xykfeb7ieeo335g3aly6vev4", title: "dootskyre", contribution: "Fallback Shortcut Creator", circle: true)
|
||||
LinkCell(imageName: "serstars", url: "https://github.com/SerStars", title: "SerStars", contribution: NSLocalizedString("Website Designer", comment: ""), circle: true)
|
||||
LinkCell(imageName: "Nathan", url: "https://github.com/verygenericname", title: "Nathan", contribution: NSLocalizedString("Exploit", comment: ""), circle: true)
|
||||
LinkCell(imageName: "duy", url: "https://github.com/khanhduytran0", title: "DuyKhanhTran", contribution: NSLocalizedString("Exploit", comment: ""), circle: true)
|
||||
LinkCell(imageName: "sky", url: "https://bsky.app/profile/did:plc:xykfeb7ieeo335g3aly6vev4", title: "dootskyre", contribution: NSLocalizedString("Fallback Shortcut Creator", comment: ""), circle: true)
|
||||
LinkCell(imageName: "POEditor", url: "https://poeditor.com/join/project/MPZOsunwVj", title: NSLocalizedString("Community Translators", comment: ""), contribution: "POEditor")
|
||||
} header: {
|
||||
Label("Credits", systemImage: "wrench.and.screwdriver")
|
||||
}
|
||||
}
|
||||
.alert(errorAlertTitle ?? "Error", isPresented: $showErrorAlert) {
|
||||
Button("OK") {}
|
||||
} message: {
|
||||
Text(errorAlertDescr ?? "???")
|
||||
}
|
||||
}
|
||||
|
||||
func startWaitForHash() {
|
||||
@@ -146,7 +144,13 @@ struct SettingsView: View {
|
||||
let contents = try String(contentsOf: filePath)
|
||||
try? FileManager.default.removeItem(at: filePath)
|
||||
await MainActor.run {
|
||||
pbHash = contents
|
||||
let items = contents.split(separator: "\n")
|
||||
if let pb = items.first {
|
||||
pbHash = String(pb)
|
||||
}
|
||||
if items.count >= 2 {
|
||||
cpHash = String(items[1])
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
|
||||
@@ -9,9 +9,11 @@ Since there is no way to read files currently, you will need a computer to get t
|
||||
Download [Nugget](https://github.com/leminlimez/Nugget) on your computer and plug in your phone. Then, navigate to the `Settings` page and click the `Pocket Poster Helper` button. Inside Pocket Poster, click the `Detect` button inside of settings.
|
||||
|
||||
## Credits
|
||||
- Translations crowdsourced using [POEditor][POEditorJoin]. Thank you everyone who assisted in the translation effort!
|
||||
- [PosterRestore][PosterRestoreDiscord] for their help with PosterBoard
|
||||
- [dootskyre][dootskyreX] for the fallback shortcut
|
||||
- [Nathan](https://github.com/verygenericname) and [DuyKhanhTran](https://github.com/khanhduytran0) for the exploit
|
||||
|
||||
[POEditorJoin]: https://poeditor.com/join/project/MPZOsunwVj
|
||||
[PosterRestoreDiscord]: https://discord.gg/gWtzTVhMvh
|
||||
[dootskyreX]: https://x.com/dootskyre
|
||||
Reference in new issue
Block a user