Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bea79eb60 | ||
|
|
e288b1f2e7 | ||
|
|
454c63d2ca | ||
|
|
fb8bd7f26e | ||
|
|
dd902d2c30 | ||
|
|
d35636c0be | ||
|
|
7434897eac | ||
|
|
395cbe5e13 | ||
|
|
71b1d0a21d | ||
|
|
22a74d5fb8 | ||
|
|
ad280b1a6b | ||
|
|
4d6b093ffd | ||
|
|
5a94ea73d9 | ||
|
|
9687a8f19d | ||
|
|
41c215776e | ||
|
|
3892e75556 | ||
|
|
99eba91bad | ||
|
|
dfee862746 | ||
|
|
faaef84376 | ||
|
|
94f78c9f4e | ||
|
|
e932b82515 | ||
|
|
edde7ddb0c | ||
|
|
cdd5cbe634 | ||
|
|
a6af03b98b | ||
|
|
c04b863efa | ||
|
|
d5833dfb81 | ||
|
|
7c2a031463 | ||
|
|
4062d084db | ||
|
|
918af3baa3 | ||
|
|
2f99384650 | ||
|
|
6e95f9336a | ||
|
|
aac867c83a | ||
|
|
f9ba2dee0d | ||
|
|
759597dfb2 | ||
|
|
f305e4b2c6 | ||
|
|
069d5cf1c3 |
No files matched your search
@@ -8,6 +8,8 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */; };
|
||||
6F09BA332E00808300CDE89C /* Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09BA322E00808300CDE89C /* Dynamic */; };
|
||||
6FB105552E26D3EC00185A5C /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = 6FB105542E26D3EC00185A5C /* CachedAsyncImage */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -30,6 +32,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6FB105552E26D3EC00185A5C /* CachedAsyncImage in Frameworks */,
|
||||
6F09BA332E00808300CDE89C /* Dynamic in Frameworks */,
|
||||
6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -74,6 +78,8 @@
|
||||
name = "Pocket Poster";
|
||||
packageProductDependencies = (
|
||||
6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */,
|
||||
6F09BA322E00808300CDE89C /* Dynamic */,
|
||||
6FB105542E26D3EC00185A5C /* CachedAsyncImage */,
|
||||
);
|
||||
productName = "Pocket Poster";
|
||||
productReference = 6F09B79D2DEB694B00CDE89C /* Pocket Poster.app */;
|
||||
@@ -105,6 +111,8 @@
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
|
||||
6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */,
|
||||
6FB105532E26D3EC00185A5C /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 6F09B79E2DEB694B00CDE89C /* Products */;
|
||||
@@ -276,12 +284,12 @@
|
||||
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",
|
||||
);
|
||||
MARKETING_VERSION = 2.0;
|
||||
MARKETING_VERSION = 3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.lemin.Pocket-Poster";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -308,12 +316,12 @@
|
||||
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",
|
||||
);
|
||||
MARKETING_VERSION = 2.0;
|
||||
MARKETING_VERSION = 3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.lemin.Pocket-Poster";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -354,6 +362,22 @@
|
||||
minimumVersion = 0.9.19;
|
||||
};
|
||||
};
|
||||
6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/mhdhejazi/Dynamic";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.2.0;
|
||||
};
|
||||
};
|
||||
6FB105532E26D3EC00185A5C /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/lorenzofiamingo/swiftui-cached-async-image";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.1.1;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -362,6 +386,16 @@
|
||||
package = 6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */;
|
||||
productName = ZIPFoundation;
|
||||
};
|
||||
6F09BA322E00808300CDE89C /* Dynamic */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */;
|
||||
productName = Dynamic;
|
||||
};
|
||||
6FB105542E26D3EC00185A5C /* CachedAsyncImage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 6FB105532E26D3EC00185A5C /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */;
|
||||
productName = CachedAsyncImage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 6F09B7952DEB694B00CDE89C /* Project object */;
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
{
|
||||
"originHash" : "d0d4edfdf2bf3cd05b3ba2dec0af1a9c271c93f944cbba8677cc647f74a6b323",
|
||||
"originHash" : "7f2d604f1affb7dc07ec1898cef3c0eaf130e2b55b5c441f445ab59c3285c1bb",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "dynamic",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mhdhejazi/Dynamic",
|
||||
"state" : {
|
||||
"revision" : "ab9a2570862d54aed2663691bb767f881226a12f",
|
||||
"version" : "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-cached-async-image",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/lorenzofiamingo/swiftui-cached-async-image",
|
||||
"state" : {
|
||||
"revision" : "467a3d17479887943ab917a379e62bbaff60ac8a",
|
||||
"version" : "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "zipfoundation",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
<key>Pocket Poster.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// CowabungaAPI.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 7/15/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
enum FilterType: String, CaseIterable {
|
||||
case random = "Random"
|
||||
case newest = "Newest"
|
||||
case oldest = "Oldest"
|
||||
}
|
||||
|
||||
class CowabungaAPI: ObservableObject {
|
||||
|
||||
static let shared = CowabungaAPI()
|
||||
|
||||
var serverURL = ""
|
||||
var session = URLSession.shared
|
||||
|
||||
func fetchWallpapers(type: DownloadableWallpaper.WallpaperType) async throws -> [DownloadableWallpaper] {
|
||||
let request = URLRequest(url: .init(string: serverURL + "wallpapers-\(type.rawValue).json")!)
|
||||
|
||||
let (data, response) = try await session.data(for: request) as! (Data, HTTPURLResponse)
|
||||
guard response.statusCode == 200 else { throw APIError.connectionFailed }
|
||||
let wallpapers = try JSONDecoder().decode([DownloadableWallpaper].self, from: data)
|
||||
|
||||
for i in wallpapers.indices {
|
||||
wallpapers[i].type = type
|
||||
}
|
||||
|
||||
return wallpapers
|
||||
}
|
||||
|
||||
func filterWallpapers(wallpapers: [DownloadableWallpaper], filterType: FilterType) -> [DownloadableWallpaper] {
|
||||
var filtered = wallpapers
|
||||
if filterType == FilterType.newest {
|
||||
filtered = filtered.reversed()
|
||||
} else if filterType == FilterType.random {
|
||||
filtered = filtered.shuffled()
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func getCommitHash() async throws -> String {
|
||||
let request = URLRequest(url: .init(string: serverURL + "https://api.github.com/repos/SerStars/nugget-wallpapers/commits/main")!)
|
||||
|
||||
let (data, response) = try await session.data(for: request) as! (Data, HTTPURLResponse)
|
||||
guard response.statusCode == 200 else { throw APIError.connectionFailed }
|
||||
guard let repoinfo = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { throw APIError.repoHashError }
|
||||
guard let hash = repoinfo["sha"] as? String else { throw APIError.repoHashError }
|
||||
return hash
|
||||
}
|
||||
|
||||
func getDownloadURLForWallpaper(wallpaper: DownloadableWallpaper) -> URL {
|
||||
if wallpaper.url.hasPrefix("https://") {
|
||||
URL(string: wallpaper.url)!
|
||||
} else {
|
||||
URL(string: serverURL + wallpaper.url)!
|
||||
}
|
||||
}
|
||||
|
||||
func getPreviewURLForWallpaper(wallpaper: DownloadableWallpaper) -> URL {
|
||||
URL(string: serverURL + wallpaper.preview)!
|
||||
}
|
||||
|
||||
init() {
|
||||
Task {
|
||||
do {
|
||||
let hash = try await getCommitHash()
|
||||
serverURL = "https://raw.githubusercontent.com/SerStars/nugget-wallpapers/\(hash)/"
|
||||
} catch {
|
||||
await UIApplication.shared.alert(body: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// DownloadableWallpaper.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 7/15/25.
|
||||
//
|
||||
|
||||
class DownloadableWallpaper: Identifiable, Codable {
|
||||
var name: String
|
||||
var description: String?
|
||||
var url: String
|
||||
var preview: String
|
||||
var authors: String?
|
||||
var type: WallpaperType?
|
||||
|
||||
init(name: String, description: String?, authors: String?, preview: String, url: String, version: String) {
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.authors = authors
|
||||
self.preview = preview
|
||||
self.url = url
|
||||
}
|
||||
|
||||
enum WallpaperType: String, Codable {
|
||||
case custom, apple, template
|
||||
}
|
||||
|
||||
func previewIsGif() -> Bool {
|
||||
return preview.hasSuffix(".gif")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//
|
||||
// CarPlayWallpaper.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CarPlayWallpaper: Identifiable {
|
||||
var id = UUID()
|
||||
var name: String
|
||||
var lightImage: UIImage
|
||||
var darkImage: UIImage
|
||||
var selectedImageDataLight: Data?
|
||||
var selectedImageDataDark: Data?
|
||||
}
|
||||
|
||||
extension CarPlayWallpaper: Reorderable {
|
||||
typealias OrderElement = String
|
||||
var orderElement: OrderElement { name }
|
||||
}
|
||||
|
||||
class CarPlayManager {
|
||||
static func supportsCarPlay() -> Bool {
|
||||
if UIDevice.current.userInterfaceIdiom != .phone {
|
||||
// CarPlay is not on iPads
|
||||
return false
|
||||
}
|
||||
if #available(iOS 19, *) {
|
||||
// get the build number
|
||||
var osVersionString = [CChar](repeating: 0, count: 16)
|
||||
var osVersionStringLen = size_t(osVersionString.count - 1)
|
||||
|
||||
let result = sysctlbyname("kern.osversion", &osVersionString, &osVersionStringLen, nil, 0)
|
||||
|
||||
if result == 0 {
|
||||
// Convert C array to String
|
||||
if let build = String(validatingUTF8: osVersionString) {
|
||||
// check build number for iOS 26 dev beta 1-3, return true if user is on that
|
||||
if build == "23A5260n" // dev beta 1
|
||||
|| build == "23A5260u" // dev beta 1 (iPhone 15/16 series)
|
||||
|| build == "23A5276f" // dev beta 2
|
||||
|| build == "23A5287g" { // dev beta 3
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
print("Failed to convert build number to String")
|
||||
}
|
||||
} else {
|
||||
print("sysctlbyname failed with error: \(String(cString: strerror(errno)))")
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func getCarPlayCacheVersion() -> String {
|
||||
// they started adding numbers to the end in iOS 18, this is to compensate for that
|
||||
if #available(iOS 19.0, *) {
|
||||
return "-12"
|
||||
} else if #available(iOS 18.0, *) {
|
||||
return "-11"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
static func getCarPlayWallpaperNames() -> [String]? {
|
||||
dlopen("/System/Library/PrivateFrameworks/CarPlayUIServices.framework/CarPlayUIServices", RTLD_GLOBAL)
|
||||
|
||||
if #available(iOS 18.0, *) {
|
||||
// iOS 18 method (it got moved)
|
||||
guard let obj = objc_getClass("CRSUISystemWallpaper") as? NSObject else { print("no class"); return nil }
|
||||
|
||||
if let success = obj.perform(Selector(("wallpapers"))), let arr = success.takeUnretainedValue() as? [NSObject] {
|
||||
var namesList: [String] = []
|
||||
for wp in arr {
|
||||
if let wpACName = wp.perform(Selector(("wallpaperAssetCatalogName"))), let result = wpACName.takeUnretainedValue() as? String {
|
||||
namesList.append(result)
|
||||
}
|
||||
}
|
||||
return namesList
|
||||
}
|
||||
} else {
|
||||
// iOS 17-
|
||||
guard let obj = objc_getClass("CRSUIWallpaperPreferences") as? NSObject else { print("no class"); return nil }
|
||||
if let success = obj.perform(Selector(("availableWallpapers"))), let arr = success.takeUnretainedValue() as? [NSObject] {
|
||||
var namesList: [String] = []
|
||||
for wp in arr {
|
||||
if let wpACName = wp.perform(Selector(("wallpaperAssetCatalogName"))), let result = wpACName.takeUnretainedValue() as? String {
|
||||
namesList.append(result)
|
||||
}
|
||||
}
|
||||
return namesList
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static func getCarPlayPhotosURL() -> URL {
|
||||
let cppURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CarPlayPhotos", conformingTo: .directory)
|
||||
// create it if it doesn't exist
|
||||
if !FileManager.default.fileExists(atPath: cppURL.path()) {
|
||||
try? FileManager.default.createDirectory(at: cppURL, withIntermediateDirectories: true)
|
||||
}
|
||||
return cppURL
|
||||
}
|
||||
|
||||
static func applyCarPlay(appHash: String, wallpapers: [CarPlayWallpaper]) throws {
|
||||
// write the image
|
||||
var toRemove: [URL] = []
|
||||
var activeWP: [String] = UserDefaults.standard.array(forKey: "ActiveCarPlayWallpapers") as? [String] ?? []
|
||||
let cppURL = getCarPlayPhotosURL()
|
||||
let cacheVer = getCarPlayCacheVersion()
|
||||
for wallpaper in wallpapers {
|
||||
if let data = wallpaper.selectedImageDataLight, let img = UIImage(data: data) {
|
||||
let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Light\(cacheVer).cpbitmap")
|
||||
img.writeToCPBitmapFile(to: imgURL.path() as NSString)
|
||||
try? data.write(to: cppURL.appendingPathComponent("\(wallpaper.name)-Light"))
|
||||
toRemove.append(imgURL)
|
||||
if !activeWP.contains(wallpaper.name) {
|
||||
activeWP.append(wallpaper.name)
|
||||
}
|
||||
}
|
||||
if let data = wallpaper.selectedImageDataDark, let img = UIImage(data: data) {
|
||||
let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Dark\(cacheVer).cpbitmap")
|
||||
img.writeToCPBitmapFile(to: imgURL.path() as NSString)
|
||||
try? data.write(to: cppURL.appendingPathComponent("\(wallpaper.name)-Dark"))
|
||||
toRemove.append(imgURL)
|
||||
if !activeWP.contains(wallpaper.name) {
|
||||
activeWP.append(wallpaper.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
UserDefaults.standard.set(activeWP, forKey: "ActiveCarPlayWallpapers")
|
||||
let test = SymHandler.getDocumentsDirectory().appendingPathComponent("Caches")
|
||||
try Data(count: 0).write(to: test)
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,62 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class DownloadManager {
|
||||
static func getWallpaperNameFromURL(string url: String) -> String {
|
||||
class DownloadManager: ObservableObject {
|
||||
static let shared = DownloadManager()
|
||||
static let exploreLink = "https://cowabun.ga/wallpapers?pocketposter=true"
|
||||
|
||||
@Published var downloadURL: String? = nil
|
||||
|
||||
func getWallpaperNameFromURL(string url: String) -> String {
|
||||
return String(url.split(separator: "/").last ?? "Unknown")
|
||||
}
|
||||
|
||||
static func downloadFromURL(_ url: URL) async throws -> URL {
|
||||
func startTendiesDownload(for url: URL) {
|
||||
// prohibit to only tendies files
|
||||
if !url.absoluteString.hasSuffix(".tendies") {
|
||||
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 {
|
||||
DispatchQueue.main.async {
|
||||
self.downloadURL = url.absoluteString.replacingOccurrences(of: "pocketposter://download?url=", with: "")
|
||||
UIApplication.shared.confirmAlert(title: NSLocalizedString("Download Tendies File", comment: ""), body: String(format: NSLocalizedString("Would you like to download the file %@?", comment: ""), "\(self.getWallpaperNameFromURL(string: self.downloadURL ?? "/Unknown"))"), onOK: {
|
||||
self.downloadWallpaper()
|
||||
}, noCancel: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadWallpaper() {
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Downloading", comment: "") + " \(getWallpaperNameFromURL(string: downloadURL ?? "/Unknown"))...", body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
|
||||
|
||||
Task {
|
||||
do {
|
||||
let newURL = try await downloadFromURL(string: downloadURL!)
|
||||
DispatchQueue.main.async {
|
||||
PosterBoardManager.shared.selectedTendies.append(newURL)
|
||||
Haptic.shared.notify(.success)
|
||||
UIApplication.shared.dismissAlert(animated: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Successfully downloaded wallpaper!", comment: ""), body: NSLocalizedString("Your downloaded .tendies will be on the Home page.", comment: "after successfully downloading tendies"))
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
Haptic.shared.notify(.error)
|
||||
UIApplication.shared.dismissAlert(animated: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Could not download wallpaper!", comment: ""), body: error.localizedDescription)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFromURL(_ url: URL) async throws -> URL {
|
||||
print("Downloading from \(url.absoluteString)")
|
||||
|
||||
let request = URLRequest(url: url)
|
||||
@@ -24,12 +73,12 @@ class DownloadManager {
|
||||
return newURL
|
||||
}
|
||||
|
||||
static func downloadFromURL(string path: String) async throws -> URL {
|
||||
func downloadFromURL(string path: String) async throws -> URL {
|
||||
guard let url = URL(string: path) else { throw URLError(.unknown) }
|
||||
return try await downloadFromURL(url)
|
||||
}
|
||||
|
||||
static func copyTendies(from url: URL) throws -> URL {
|
||||
func copyTendies(from url: URL) throws -> URL {
|
||||
// scope the resource
|
||||
let accessing = url.startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
|
||||
@@ -35,6 +35,7 @@ class PosterBoardManager: ObservableObject {
|
||||
guard let obj = objc_getClass("IPSettingsUtilities") as? NSObject else { return false }
|
||||
langManager = obj
|
||||
} else {
|
||||
// TODO: Need to find the correct class for this on iOS 17
|
||||
guard let obj = objc_getClass("PSLanguageSelector") as? NSObject else { return false }
|
||||
langManager = obj
|
||||
}
|
||||
@@ -102,7 +103,6 @@ class PosterBoardManager: ObservableObject {
|
||||
if fileName.lowercased() == "container" {
|
||||
// container support, find the extensions
|
||||
let extDir = dir.appending(path: "Library/Application Support/PRBPosterExtensionDataStore/61/Extensions")
|
||||
print(extDir.absoluteString)
|
||||
var retList: [String: [URL]] = [:]
|
||||
for ext in try FileManager.default.contentsOfDirectory(at: extDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
|
||||
let descrDir = ext.appendingPathComponent("descriptors")
|
||||
@@ -239,7 +239,9 @@ class PosterBoardManager: ObservableObject {
|
||||
SymHandler.cleanup()
|
||||
let docDir = SymHandler.getDocumentsDirectory()
|
||||
for file in try FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil) {
|
||||
if file.lastPathComponent != "CarPlayPhotos" {
|
||||
try FileManager.default.removeItem(at: file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,13 @@ class SymHandler {
|
||||
return getDocumentsDirectory()
|
||||
}
|
||||
|
||||
static func getPosterBoardHashURL() -> URL {
|
||||
return getLCDocumentsDirectory().appendingPathComponent("NuggetPosterBoardHash")
|
||||
}
|
||||
static func getCarPlayHashURL() -> URL {
|
||||
return getLCDocumentsDirectory().appendingPathComponent("NuggetCarPlayWallpaperHash")
|
||||
}
|
||||
|
||||
private static func getSymlinkURL() -> URL {
|
||||
return getLCDocumentsDirectory().appendingPathComponent(".Trash", conformingTo: .symbolicLink)
|
||||
}
|
||||
@@ -43,10 +50,18 @@ class SymHandler {
|
||||
return try createSymlink(to: "/var/mobile/Containers/Data/Application/\(appHash)")
|
||||
}
|
||||
|
||||
static func getExtensionVersion() -> String {
|
||||
if #available(iOS 17.0, *) {
|
||||
return "61"
|
||||
}
|
||||
return "59"
|
||||
}
|
||||
|
||||
static func createDescriptorsSymlink(appHash: String, ext: String) throws -> URL {
|
||||
// create a symlink directly to the descriptors
|
||||
print("linking to \(appHash)/Library/Application Support/PRBPosterExtensionDataStore/61/Extensions/\(ext)/descriptors")
|
||||
return try createAppSymlink(for: "\(appHash)/Library/Application Support/PRBPosterExtensionDataStore/61/Extensions/\(ext)/descriptors")
|
||||
let extVer = SymHandler.getExtensionVersion()
|
||||
print("linking to \(appHash)/Library/Application Support/PRBPosterExtensionDataStore/\(extVer)/Extensions/\(ext)/descriptors")
|
||||
return try createAppSymlink(for: "\(appHash)/Library/Application Support/PRBPosterExtensionDataStore/\(extVer)/Extensions/\(ext)/descriptors")
|
||||
}
|
||||
|
||||
static func cleanup() {
|
||||
|
||||
@@ -102,7 +102,8 @@ class VideoHandler {
|
||||
print("Failed to get video track")
|
||||
throw AVError(.decodeFailed)
|
||||
}
|
||||
let size = track.naturalSize.applying(track.preferredTransform)
|
||||
let preferredTransform = track.preferredTransform
|
||||
let size = track.naturalSize.applying(preferredTransform)
|
||||
let width = Int(abs(size.width))
|
||||
let height = Int(abs(size.height))
|
||||
let fps = track.nominalFrameRate
|
||||
@@ -145,7 +146,7 @@ class VideoHandler {
|
||||
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 ciImage = CIImage(cvPixelBuffer: imageBuffer).transformed(by: preferredTransform)
|
||||
let context = CIContext()
|
||||
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
|
||||
if let img = cgImage.jpg {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// APIError.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 7/15/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum APIError: LocalizedError {
|
||||
// Throw when cannot connect to server
|
||||
case connectionFailed
|
||||
|
||||
// Throw when it fails to get the repo hash
|
||||
case repoHashError
|
||||
|
||||
// Throw in all other cases
|
||||
case unexpected(info: String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .connectionFailed:
|
||||
return NSLocalizedString("Could not connect to server", comment: "")
|
||||
case .repoHashError:
|
||||
return NSLocalizedString("Unable to obtain repo hash. Maybe update to the latest version?", comment: "")
|
||||
case .unexpected(let info):
|
||||
return info
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Reorderable.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol Reorderable {
|
||||
associatedtype OrderElement: Equatable
|
||||
var orderElement: OrderElement { get }
|
||||
}
|
||||
|
||||
extension Array where Element: Reorderable {
|
||||
|
||||
func reorder(by preferredOrder: [Element.OrderElement]) -> [Element] {
|
||||
sorted {
|
||||
guard let first = preferredOrder.firstIndex(of: $0.orderElement) else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let second = preferredOrder.firstIndex(of: $1.orderElement) else {
|
||||
return true
|
||||
}
|
||||
|
||||
return first < second
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// URLCache+imageCache.swift
|
||||
// Cowabunga
|
||||
//
|
||||
// Created by sourcelocation on 02/02/2023.
|
||||
//
|
||||
|
||||
import CachedAsyncImage
|
||||
import Foundation
|
||||
|
||||
extension URLCache {
|
||||
|
||||
static let imageCache = URLCache(memoryCapacity: 512*1000*1000, diskCapacity: 10*1000*1000*1000)
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// OpaqueButton.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OpaqueButton: ButtonStyle {
|
||||
var color: Color
|
||||
var textColor: Color
|
||||
var fullwidth: Bool = false
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
ZStack {
|
||||
if fullwidth {
|
||||
configuration.label
|
||||
.padding(15)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(color)
|
||||
.cornerRadius(8)
|
||||
.foregroundColor(textColor)
|
||||
} else {
|
||||
configuration.label
|
||||
.padding(15)
|
||||
.background(color)
|
||||
.cornerRadius(8)
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(color: Color = .blue, textColor: Color = .primary, fullwidth: Bool = false) {
|
||||
self.color = color
|
||||
self.textColor = textColor
|
||||
self.fullwidth = fullwidth
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// CornerRadius++.swift
|
||||
// Cowabunga
|
||||
//
|
||||
// Created by sourcelocation on 30/01/2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||
clipShape( RoundedCorner(radius: radius, corners: corners) )
|
||||
}
|
||||
}
|
||||
|
||||
struct RoundedCorner: Shape {
|
||||
|
||||
var radius: CGFloat = .infinity
|
||||
var corners: UIRectCorner = .allCorners
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3047
-50
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 06/04/2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public extension GIFImage {
|
||||
|
||||
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
|
||||
/// into frames and stream them based in the "Delay" key packaged on which frame item.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: URL string of the image. If the string cannot be parsed into URL, this constructor early aborts and returns `nil`
|
||||
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
|
||||
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
|
||||
/// - placeholder: Image to be used before the source is loaded
|
||||
/// - errorImage: If the stream fails, this image is used
|
||||
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
|
||||
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
|
||||
init?(
|
||||
url: String,
|
||||
animate: Bool,
|
||||
loop: Bool,
|
||||
placeholder: RawImage = RawImage(),
|
||||
errorImage: RawImage? = nil,
|
||||
frameRate: FrameRate = .dynamic,
|
||||
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
|
||||
) {
|
||||
self.init(
|
||||
url: url,
|
||||
animate: .constant(animate),
|
||||
loop: .constant(loop),
|
||||
placeholder: placeholder,
|
||||
errorImage: errorImage,
|
||||
frameRate: frameRate,
|
||||
loopAction: loopAction
|
||||
)
|
||||
}
|
||||
|
||||
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
|
||||
/// into frames and stream them based in the "Delay" key packaged on which frame item.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: URL string of the image. If the string cannot be parsed into URL, this constructor early aborts and returns `nil`
|
||||
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
|
||||
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
|
||||
/// - placeholder: Image to be used before the source is loaded
|
||||
/// - errorImage: If the stream fails, this image is used
|
||||
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
|
||||
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
|
||||
init?(
|
||||
url: String,
|
||||
animate: Binding<Bool> = Binding.constant(true),
|
||||
loop: Binding<Bool> = Binding.constant(true),
|
||||
placeholder: RawImage = RawImage(),
|
||||
errorImage: RawImage? = nil,
|
||||
frameRate: FrameRate = .dynamic,
|
||||
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
|
||||
) {
|
||||
guard let resolvedURL = URL(string: url) else {
|
||||
return nil
|
||||
}
|
||||
self.init(
|
||||
source: GIFSource.remoteURL(resolvedURL),
|
||||
animate: animate,
|
||||
loop: loop,
|
||||
placeholder: placeholder,
|
||||
errorImage: errorImage,
|
||||
frameRate: frameRate,
|
||||
loopAction: loopAction
|
||||
)
|
||||
}
|
||||
|
||||
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
|
||||
/// into frames and stream them based in the "Delay" key packaged on which frame item.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: URL of the image. The response is cached using `URLCache`
|
||||
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
|
||||
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
|
||||
/// - placeholder: Image to be used before the source is loaded
|
||||
/// - errorImage: If the stream fails, this image is used
|
||||
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
|
||||
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
|
||||
init(
|
||||
url: URL,
|
||||
animate: Bool = true,
|
||||
loop: Bool = true,
|
||||
placeholder: RawImage = RawImage(),
|
||||
errorImage: RawImage? = nil,
|
||||
frameRate: FrameRate = .dynamic,
|
||||
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
|
||||
) {
|
||||
self.init(
|
||||
url: url,
|
||||
animate: .constant(animate),
|
||||
loop: .constant(loop),
|
||||
placeholder: placeholder,
|
||||
errorImage: errorImage,
|
||||
frameRate: frameRate,
|
||||
loopAction: loopAction
|
||||
)
|
||||
}
|
||||
|
||||
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
|
||||
/// into frames and stream them based in the "Delay" key packaged on which frame item.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: URL of the image. The response is cached using `URLCache`
|
||||
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
|
||||
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
|
||||
/// - placeholder: Image to be used before the source is loaded
|
||||
/// - errorImage: If the stream fails, this image is used
|
||||
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
|
||||
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
|
||||
init(
|
||||
url: URL,
|
||||
animate: Binding<Bool> = Binding.constant(true),
|
||||
loop: Binding<Bool> = Binding.constant(true),
|
||||
placeholder: RawImage = RawImage(),
|
||||
errorImage: RawImage? = nil,
|
||||
frameRate: FrameRate = .dynamic,
|
||||
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
|
||||
) {
|
||||
self.init(
|
||||
source: GIFSource.remoteURL(url),
|
||||
animate: animate,
|
||||
loop: loop,
|
||||
placeholder: placeholder,
|
||||
errorImage: errorImage,
|
||||
frameRate: frameRate,
|
||||
loopAction: loopAction
|
||||
)
|
||||
}
|
||||
|
||||
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
|
||||
/// into frames and stream them based in the "Delay" key packaged on which frame item.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - request: `URLRequest` of the image. The response is cached using `URLCache`
|
||||
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
|
||||
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
|
||||
/// - placeholder: Image to be used before the source is loaded
|
||||
/// - errorImage: If the stream fails, this image is used
|
||||
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
|
||||
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
|
||||
init(
|
||||
request: URLRequest,
|
||||
animate: Binding<Bool> = Binding.constant(true),
|
||||
loop: Binding<Bool> = Binding.constant(true),
|
||||
placeholder: RawImage = RawImage(),
|
||||
errorImage: RawImage? = nil,
|
||||
frameRate: FrameRate = .dynamic,
|
||||
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
|
||||
) {
|
||||
self.init(
|
||||
source: GIFSource.remoteRequest(request),
|
||||
animate: animate,
|
||||
loop: loop,
|
||||
placeholder: placeholder,
|
||||
errorImage: errorImage,
|
||||
frameRate: frameRate,
|
||||
loopAction: loopAction
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import SwiftUI
|
||||
|
||||
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
|
||||
/// into frames and stream them based in the "Delay" key packaged on which frame item. The view will use the `ImageLoader` from the environment
|
||||
/// to convert the fetch the `Data`
|
||||
public struct GIFImage: View {
|
||||
public let source: GIFSource
|
||||
public let placeholder: RawImage
|
||||
public let errorImage: RawImage?
|
||||
private let presentationController: PresentationController
|
||||
|
||||
@Environment(\.imageLoader) var imageLoader
|
||||
@State @MainActor private var frame: RawImage?
|
||||
@Binding public var loop: Bool
|
||||
@Binding public var animate: Bool
|
||||
@State private var presentationTask: Task<(), Never>?
|
||||
|
||||
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
|
||||
/// into frames and stream them based in the "Delay" key packaged on which frame item.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: Source of the image. If the source is remote, the response is cached using `URLCache`
|
||||
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
|
||||
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
|
||||
/// - placeholder: Image to be used before the source is loaded
|
||||
/// - errorImage: If the stream fails, this image is used
|
||||
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
|
||||
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
|
||||
public init(
|
||||
source: GIFSource,
|
||||
animate: Bool,
|
||||
loop: Bool,
|
||||
placeholder: RawImage = RawImage(),
|
||||
errorImage: RawImage? = nil,
|
||||
frameRate: FrameRate = .dynamic,
|
||||
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
|
||||
) {
|
||||
self.init(
|
||||
source: source,
|
||||
animate: .constant(animate),
|
||||
loop: .constant(loop),
|
||||
placeholder: placeholder,
|
||||
errorImage: errorImage,
|
||||
frameRate: frameRate,
|
||||
loopAction: loopAction
|
||||
)
|
||||
}
|
||||
|
||||
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
|
||||
/// into frames and stream them based in the "Delay" key packaged on which frame item.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: Source of the image. If the source is remote, the response is cached using `URLCache`
|
||||
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
|
||||
/// - placeholder: Image to be used before the source is loaded
|
||||
/// - errorImage: If the stream fails, this image is used
|
||||
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
|
||||
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
|
||||
public init(
|
||||
source: GIFSource,
|
||||
animate: Binding<Bool> = Binding.constant(true),
|
||||
loop: Binding<Bool> = Binding.constant(true),
|
||||
placeholder: RawImage = RawImage(),
|
||||
errorImage: RawImage? = nil,
|
||||
frameRate: FrameRate = .dynamic,
|
||||
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
|
||||
) {
|
||||
self.source = source
|
||||
self._animate = animate
|
||||
self._loop = loop
|
||||
self.placeholder = placeholder
|
||||
self.errorImage = errorImage
|
||||
|
||||
self.presentationController = PresentationController(
|
||||
source: source,
|
||||
frameRate: frameRate,
|
||||
animate: animate,
|
||||
loop: loop,
|
||||
action: loopAction
|
||||
)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Image.loadImage(with: frame ?? placeholder)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.onChange(of: loop) { newValue in
|
||||
handle(loop: newValue)
|
||||
}
|
||||
.onChange(of: animate) { newValue in
|
||||
handle(animate: newValue)
|
||||
}
|
||||
.task(id: source, load)
|
||||
}
|
||||
|
||||
private func handle(animate: Bool) {
|
||||
if animate {
|
||||
load()
|
||||
} else {
|
||||
presentationTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(loop: Bool) {
|
||||
if loop { load() }
|
||||
}
|
||||
|
||||
@Sendable private func load() {
|
||||
presentationTask?.cancel()
|
||||
presentationTask = Task { await presentationController.start(
|
||||
imageLoader: imageLoader,
|
||||
fallbackImage: errorImage ?? placeholder,
|
||||
frameUpdate: setFrame(_:)
|
||||
)}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Sendable private func setFrame(_ frame: RawImage) async {
|
||||
self.frame = frame
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
let placeholder = RawImage.create(symbol: "photo.circle.fill")!
|
||||
let error = RawImage.create(symbol: "xmark.octagon")
|
||||
let gifURL = "https://raw.githubusercontent.com/igorcferreira/GIFImage/main/Tests/test.gif"
|
||||
#Preview("Raw URL") {
|
||||
GIFImage(url: gifURL, placeholder: placeholder, errorImage: error)
|
||||
}
|
||||
#Preview("Limited 5 FPS") {
|
||||
GIFImage(url: gifURL, placeholder: placeholder, errorImage: error, frameRate: .limited(fps: 5))
|
||||
}
|
||||
#Preview("Limited to 30 FPS") {
|
||||
GIFImage(url: gifURL, placeholder: placeholder, errorImage: error, frameRate: .static(fps: 30))
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 05/04/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ImageIO
|
||||
|
||||
public actor CGImageSourceFrameSequence: AsyncSequence {
|
||||
public typealias Element = ImageFrame
|
||||
|
||||
public enum LoadError: Error {
|
||||
case invalidData
|
||||
case emptyData
|
||||
}
|
||||
|
||||
public let source: CGImageSource
|
||||
|
||||
public init(data: Data) throws {
|
||||
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
||||
throw LoadError.invalidData
|
||||
}
|
||||
|
||||
switch CGImageSourceGetStatus(source) {
|
||||
case .statusComplete: break
|
||||
case .statusReadingHeader: break
|
||||
case .statusIncomplete: throw LoadError.emptyData
|
||||
case .statusInvalidData: throw LoadError.invalidData
|
||||
case .statusUnexpectedEOF: throw LoadError.invalidData
|
||||
case .statusUnknownType: throw LoadError.invalidData
|
||||
@unknown default: throw LoadError.invalidData
|
||||
}
|
||||
|
||||
self.init(source: source)
|
||||
}
|
||||
|
||||
public init(source: CGImageSource) {
|
||||
self.source = source
|
||||
}
|
||||
|
||||
nonisolated public func makeAsyncIterator() -> CGImageSourceIterator {
|
||||
CGImageSourceIterator(source: source)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 05/04/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ImageIO
|
||||
|
||||
public actor CGImageSourceIterator: AsyncIteratorProtocol {
|
||||
|
||||
public let frameCount: Int
|
||||
public let source: CGImageSource
|
||||
public private(set) var currentFrame: Int
|
||||
|
||||
public init(source: CGImageSource) {
|
||||
self.source = source
|
||||
self.frameCount = CGImageSourceGetCount(source)
|
||||
self.currentFrame = 0
|
||||
}
|
||||
|
||||
public func next() async throws -> ImageFrame? {
|
||||
guard currentFrame < frameCount else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let frame: ImageFrame? = if let image = CGImageSourceCreateImageAtIndex(source, currentFrame, nil) {
|
||||
ImageFrame(image: image, interval: source.intervalAtIndex(currentFrame))
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
currentFrame += 1
|
||||
return frame
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 05/04/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Object responsible to load a `GIFSource` into a `ImageFrame` stream.
|
||||
public struct ImageLoader {
|
||||
public let session: URLSession
|
||||
public let cache: URLCache
|
||||
public let fileManager: FileManager
|
||||
|
||||
public init(session: URLSession = .shared, cache: URLCache = .shared, fileManager: FileManager = .default) {
|
||||
self.session = session
|
||||
self.cache = cache
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
public func load(source: GIFSource) async throws -> CGImageSourceFrameSequence {
|
||||
let data = try await source.loadData(session: session, cache: cache, fileManager: fileManager)
|
||||
return try data.imageAsyncSequence()
|
||||
}
|
||||
}
|
||||
|
||||
private extension GIFSource {
|
||||
func loadData(session: URLSession, cache: URLCache, fileManager: FileManager) async throws -> Data {
|
||||
switch self {
|
||||
case let .static(data): data
|
||||
case .remote(let url), .remoteURL(let url): try await url.loadData(session: session, cache: cache)
|
||||
case let .remoteRequest(request): try await request.loadData(session: session, cache: cache)
|
||||
case let .local(filePath): try await filePath.loadData(fileManager: fileManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
func loadData(fileManager: FileManager) async throws -> Data {
|
||||
guard fileManager.fileExists(atPath: self) else {
|
||||
throw URLError(.fileDoesNotExist)
|
||||
}
|
||||
guard let data = fileManager.contents(atPath: self) else {
|
||||
throw URLError(.cannotOpenFile)
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
private extension URL {
|
||||
func loadData(session: URLSession, cache: URLCache) async throws -> Data {
|
||||
let request = URLRequest(url: self)
|
||||
return try await request.loadData(session: session, cache: cache)
|
||||
}
|
||||
}
|
||||
|
||||
private extension URLRequest {
|
||||
func loadData(session: URLSession, cache: URLCache) async throws -> Data {
|
||||
if let cache = cache.cachedResponse(for: self) {
|
||||
return cache.data
|
||||
}
|
||||
|
||||
let (data, response) = try await session.data(for: self)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
guard httpResponse.isSuccess else {
|
||||
throw URLError(.init(rawValue: httpResponse.statusCode))
|
||||
}
|
||||
cache.storeCachedResponse(CachedURLResponse(response: response, data: data), for: self)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
private extension HTTPURLResponse {
|
||||
var isSuccess: Bool {
|
||||
200..<300 ~= statusCode
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 9/9/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
private let kDefaultGIFFrameInterval: TimeInterval = 1.0 / 24.0
|
||||
|
||||
struct PresentationController {
|
||||
let source: GIFSource
|
||||
let frameRate: FrameRate
|
||||
let action: (GIFSource) async throws -> Void
|
||||
@Binding var animate: Bool
|
||||
@Binding var loop: Bool
|
||||
|
||||
init(
|
||||
source: GIFSource,
|
||||
frameRate: FrameRate,
|
||||
animate: Binding<Bool>,
|
||||
loop: Binding<Bool>,
|
||||
action: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
|
||||
) {
|
||||
self.source = source
|
||||
self.action = action
|
||||
self.frameRate = frameRate
|
||||
self._animate = animate
|
||||
self._loop = loop
|
||||
}
|
||||
|
||||
func start(imageLoader: ImageLoader, fallbackImage: RawImage, frameUpdate: (RawImage) async -> Void) async {
|
||||
do {
|
||||
repeat {
|
||||
for try await imageFrame in try await imageLoader.load(source: source) {
|
||||
try await update(imageFrame, frameUpdate: frameUpdate)
|
||||
if !animate { break }
|
||||
}
|
||||
try await action(source)
|
||||
} while(self.loop && self.animate)
|
||||
} catch {
|
||||
if !(error is CancellationError) {
|
||||
await frameUpdate(fallbackImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func update(_ imageFrame: ImageFrame, frameUpdate: (RawImage) async -> Void) async throws {
|
||||
await frameUpdate(RawImage.create(with: imageFrame.image))
|
||||
let calculatedInterval = imageFrame.interval ?? kDefaultGIFFrameInterval
|
||||
let interval: Double
|
||||
switch frameRate {
|
||||
case .static(let fps):
|
||||
interval = (1.0 / Double(fps))
|
||||
case .limited(let fps):
|
||||
let intervalLimit = (1.0 / Double(fps))
|
||||
interval = max(calculatedInterval, intervalLimit)
|
||||
case .dynamic:
|
||||
interval = imageFrame.interval ?? kDefaultGIFFrameInterval
|
||||
}
|
||||
try await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000.0))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 05/04/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ImageIO
|
||||
|
||||
extension CFString {
|
||||
var asKey: UnsafeMutableRawPointer {
|
||||
return Unmanaged.passUnretained(self).toOpaque()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 05/04/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ImageIO
|
||||
|
||||
extension CGImageSource {
|
||||
func intervalAtIndex(_ index: Int) -> TimeInterval? {
|
||||
guard let properties = CGImageSourceCopyPropertiesAtIndex(self, index, nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let gifProperties = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary.asKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let dictionary = unsafeBitCast(gifProperties, to: CFDictionary.self)
|
||||
|
||||
let pointer: UnsafeRawPointer
|
||||
if let delay = CFDictionaryGetValue(dictionary, kCGImagePropertyGIFDelayTime.asKey) {
|
||||
pointer = delay
|
||||
} else if let unclampedDelay = CFDictionaryGetValue(dictionary, kCGImagePropertyGIFUnclampedDelayTime.asKey) {
|
||||
pointer = unclampedDelay
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let interval = unsafeBitCast(pointer, to: AnyObject.self).doubleValue ?? 0.0
|
||||
if interval > 0.0 {
|
||||
return interval
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 05/04/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ImageIO
|
||||
|
||||
extension Data {
|
||||
func imageAsyncSequence() throws -> CGImageSourceFrameSequence {
|
||||
return try CGImageSourceFrameSequence(data: self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 05/04/2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct GIFImageEnvironment: EnvironmentKey {
|
||||
static var defaultValue: ImageLoader = {
|
||||
return ImageLoader()
|
||||
}()
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var imageLoader: ImageLoader {
|
||||
get { self[GIFImageEnvironment.self] }
|
||||
set { self[GIFImageEnvironment.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
@ViewBuilder
|
||||
func with(imageLoader: ImageLoader) -> some View {
|
||||
environment(\.imageLoader, imageLoader)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 06/04/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum FrameRate {
|
||||
case dynamic
|
||||
case limited(fps: Int)
|
||||
case `static`(fps: Int)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 29/10/2020.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum GIFSource: Equatable {
|
||||
@available(*, deprecated, renamed: "remoteURL(_:)")
|
||||
case remote(url: URL)
|
||||
case remoteURL(_ url: URL)
|
||||
case remoteRequest(_ request: URLRequest)
|
||||
case local(filePath: String)
|
||||
case `static`(data: Data)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 29/10/2020.
|
||||
//
|
||||
|
||||
import ImageIO
|
||||
import Foundation
|
||||
|
||||
public struct ImageFrame {
|
||||
public let image: CGImage
|
||||
public let interval: TimeInterval?
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor Ferreira on 05/04/2022.
|
||||
//
|
||||
|
||||
import ImageIO
|
||||
import SwiftUI
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
public typealias RawImage = NSImage
|
||||
#else
|
||||
import UIKit
|
||||
public typealias RawImage = UIImage
|
||||
#endif
|
||||
|
||||
public extension RawImage {
|
||||
static func create(with cgImage: CGImage) -> RawImage {
|
||||
#if os(macOS)
|
||||
return RawImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
|
||||
#else
|
||||
return RawImage(cgImage: cgImage)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func create(symbol: String) -> RawImage? {
|
||||
#if os(macOS)
|
||||
return RawImage(systemSymbolName: symbol, accessibilityDescription: nil)
|
||||
#else
|
||||
return RawImage(systemName: symbol)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public extension Image {
|
||||
static func loadImage(with image: RawImage) -> Image {
|
||||
#if os(macOS)
|
||||
return Image(nsImage: image)
|
||||
#else
|
||||
return Image(uiImage: image)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,12 @@
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"bg" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"cs" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
@@ -71,12 +77,30 @@
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"fr-CH" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"fr-LU" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"hu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
@@ -125,6 +149,12 @@
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"zh-CN" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pocket Poster"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
@@ -153,6 +183,12 @@
|
||||
"value" : "خلفيات Nugget"
|
||||
}
|
||||
},
|
||||
"bg" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Wallpaper"
|
||||
}
|
||||
},
|
||||
"cs" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
@@ -195,12 +231,30 @@
|
||||
"value" : "Fond d'écran Nugget"
|
||||
}
|
||||
},
|
||||
"fr-CH" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Wallpaper"
|
||||
}
|
||||
},
|
||||
"fr-LU" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Wallpaper"
|
||||
}
|
||||
},
|
||||
"hu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget Háttérkép"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sfondo di Nugget"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
@@ -249,6 +303,12 @@
|
||||
"value" : "Hình nền Nugget"
|
||||
}
|
||||
},
|
||||
"zh-CN" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nugget 墙纸"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
||||
@@ -10,10 +10,13 @@ import SwiftUI
|
||||
@main
|
||||
struct Pocket_PosterApp: App {
|
||||
// Prefs
|
||||
@ObservedObject var dlManager = DownloadManager.shared
|
||||
|
||||
@AppStorage("finishedTutorial") var finishedTutorial: Bool = false
|
||||
@AppStorage("pbHash") var pbHash: String = ""
|
||||
|
||||
@State var downloadURL: String? = nil
|
||||
@State var checkedForUpdate: Bool = false
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
@@ -26,20 +29,33 @@ struct Pocket_PosterApp: App {
|
||||
}
|
||||
.transition(.opacity)
|
||||
.animation(.easeOut(duration: 0.5), value: finishedTutorial)
|
||||
.onAppear {
|
||||
// check for update
|
||||
if !checkedForUpdate {
|
||||
checkedForUpdate = true
|
||||
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, let url = URL(string: "https://api.github.com/repos/leminlimez/Pocket-Poster/releases/latest") {
|
||||
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
|
||||
guard let data = data else { return }
|
||||
|
||||
if let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
|
||||
if (json["tag_name"] as? String)?.replacingOccurrences(of: "v", with: "").compare(version, options: .numeric) == .orderedDescending {
|
||||
UIApplication.shared.confirmAlert(
|
||||
title: NSLocalizedString("Update Available", comment: "app update available on GitHub"),
|
||||
body: String(format: NSLocalizedString("Pocket Poster %@ is available, do you want to visit releases page?", comment: "app update available on GitHub"), json["tag_name"] as? String ?? "update"),
|
||||
onOK: {
|
||||
UIApplication.shared.open(URL(string: "https://github.com/leminlimez/Pocket-Poster/releases/latest")!)
|
||||
}, noCancel: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
.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: 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: "")
|
||||
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)
|
||||
}
|
||||
dlManager.startTendiesDownload(for: url)
|
||||
}
|
||||
// App Hash URL
|
||||
else if url.absoluteString.starts(with: "pocketposter://app-hash?uuid=") {
|
||||
@@ -51,7 +67,7 @@ struct Pocket_PosterApp: App {
|
||||
} else {
|
||||
// copy it over to the KFC bucket
|
||||
do {
|
||||
let newURL = try DownloadManager.copyTendies(from: url)
|
||||
let newURL = try DownloadManager.shared.copyTendies(from: url)
|
||||
PosterBoardManager.shared.selectedTendies.append(newURL)
|
||||
Haptic.shared.notify(.success)
|
||||
UIApplication.shared.alert(title: String(format: NSLocalizedString("Successfully imported %@", comment: ""), "\(url.lastPathComponent)"), body: "")
|
||||
@@ -65,24 +81,6 @@ struct Pocket_PosterApp: App {
|
||||
}
|
||||
}
|
||||
|
||||
func downloadWallpaper() {
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Downloading", comment: "") + " \(DownloadManager.getWallpaperNameFromURL(string: downloadURL ?? "/Unknown"))...", body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
|
||||
|
||||
Task {
|
||||
do {
|
||||
let newURL = try await DownloadManager.downloadFromURL(string: downloadURL!)
|
||||
PosterBoardManager.shared.selectedTendies.append(newURL)
|
||||
Haptic.shared.notify(.success)
|
||||
UIApplication.shared.dismissAlert(animated: true)
|
||||
} catch {
|
||||
Haptic.shared.notify(.error)
|
||||
UIApplication.shared.dismissAlert(animated: true)
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Could not download wallpaper!", comment: ""), body: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
// clear the videos cache
|
||||
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent("Videos", conformingTo: .directory))
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// WebView.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 7/6/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct WallpaperWebView: UIViewRepresentable {
|
||||
let url: URL
|
||||
@Binding var isLoading: Bool
|
||||
@Binding var error: Error?
|
||||
|
||||
init(_ url: URL, isLoading: Binding<Bool>, error: Binding<Error?>) {
|
||||
self.url = url
|
||||
self._isLoading = isLoading
|
||||
self._error = error
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
func makeUIView(context: Context) -> some UIView {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.setURLSchemeHandler(AssetsSchemeHandler(), forURLScheme: "pocketposter")
|
||||
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
webView.navigationDelegate = context.coordinator
|
||||
webView.load(URLRequest(url: url))
|
||||
|
||||
return webView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIViewType, context: Context) {}
|
||||
|
||||
class Coordinator: NSObject, WKNavigationDelegate {
|
||||
var parent: WallpaperWebView
|
||||
init(_ parent: WallpaperWebView) {
|
||||
self.parent = parent
|
||||
}
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
parent.isLoading = true
|
||||
}
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
parent.isLoading = false
|
||||
}
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
return
|
||||
let nsError = error as NSError
|
||||
if nsError.code == NSURLErrorCancelled { return }
|
||||
print("loading error: \(error)")
|
||||
parent.isLoading = false
|
||||
parent.error = error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AssetsSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
||||
DispatchQueue.global().async {
|
||||
print("handling scheme")
|
||||
guard let url = urlSchemeTask.request.url, url.absoluteString.starts(with: "pocketposter://download") else { return }
|
||||
|
||||
print("url: \(url.absoluteString)")
|
||||
DownloadManager.shared.startTendiesDownload(for: url)
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,7 @@ struct ContentView: View {
|
||||
.buttonStyle(TintedButton(color: .blue, fullwidth: true))
|
||||
}
|
||||
Button(action: {
|
||||
if #available(iOS 18.0, *) {
|
||||
guard let lang = UserDefaults.standard.stringArray(forKey: "AppleLanguages")?.first else {
|
||||
hideResetHelp = false // fallback to tutorial
|
||||
return
|
||||
@@ -110,6 +111,9 @@ struct ContentView: View {
|
||||
UIApplication.shared.alert(body: "The API failed to call correctly.\nSystem Locale Code: \(lang)")
|
||||
}
|
||||
}, noCancel: false)
|
||||
} else {
|
||||
hideResetHelp = false
|
||||
}
|
||||
}) {
|
||||
Label("Reset Collections", systemImage: "arrow.clockwise.circle")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
//
|
||||
// CarPlayView.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 6/19/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
struct CarPlayView: View {
|
||||
@State var wallpapers: [CarPlayWallpaper] = []
|
||||
@State var didChange: Bool = false
|
||||
@State var showDark: Bool = false
|
||||
|
||||
@AppStorage("cpHash") var cpHash: String = ""
|
||||
@State var activeWallpapers: [String] = []
|
||||
@ObservedObject var pbManager = PosterBoardManager.shared
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
ScrollView {
|
||||
if wallpapers.isEmpty {
|
||||
ProgressView()
|
||||
} else {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150, maximum: 250))]) {
|
||||
ForEach($wallpapers) { wallpaper in
|
||||
ZStack {
|
||||
WallpaperView(wallpaper: wallpaper, didChange: $didChange, showDark: $showDark)
|
||||
.disabled(activeWallpapers.contains(wallpaper.name.wrappedValue))
|
||||
// The checkmark
|
||||
if activeWallpapers.contains(wallpaper.name.wrappedValue) {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.foregroundStyle(.black.opacity(0.4))
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.title)
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
Spacer()
|
||||
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 CarPlayManager.applyCarPlay(appHash: cpHash, wallpapers: wallpapers)
|
||||
SymHandler.cleanup() // just to be extra sure
|
||||
UIApplication.shared.dismissAlert(animated: false)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
|
||||
activeWallpapers = UserDefaults.standard.array(forKey: "ActiveCarPlayWallpapers") as? [String] ?? []
|
||||
Haptic.shared.notify(.success)
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Success!", comment: ""), body: NSLocalizedString("You can now choose your wallpapers in the CarPlay settings in your car.", comment: ""))
|
||||
})
|
||||
} catch CocoaError.fileWriteUnknown {
|
||||
presentError(ApplyError.wrongAppHash)
|
||||
} catch CocoaError.fileWriteFileExists {
|
||||
presentError(ApplyError.collectionsNeedsReset)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
presentError(ApplyError.unexpected(info: error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}) {
|
||||
if cpHash == "" {
|
||||
Text("Enter your CarPlayWallpaper app hash in Settings.")
|
||||
} else {
|
||||
Label("Apply CarPlay Wallpapers", systemImage: "checkmark.circle")
|
||||
}
|
||||
}
|
||||
.buttonStyle(OpaqueButton(color: cpHash == "" ? .red : .blue, fullwidth: true))
|
||||
.disabled(cpHash == "")
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 25)
|
||||
.transition(.move(edge: .bottom))
|
||||
.animation(.easeIn, value: didChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("CarPlay Wallpapers")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing, content: {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
showDark.toggle()
|
||||
}
|
||||
}) {
|
||||
Image(systemName: showDark ? "moon" : "sun.max")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if wallpapers.isEmpty {
|
||||
// load active wallpapers from user defaults
|
||||
activeWallpapers = UserDefaults.standard.array(forKey: "ActiveCarPlayWallpapers") as? [String] ?? []
|
||||
// load the wallpapers
|
||||
let cppURL = CarPlayManager.getCarPlayPhotosURL()
|
||||
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: "")
|
||||
// fetch the selected image if they exist
|
||||
let selectedLight: Data? = try? Data(contentsOf: cppURL.appendingPathComponent("\(name)-Light"))
|
||||
let selectedDark: Data? = try? Data(contentsOf: cppURL.appendingPathComponent("\(name)-Dark"))
|
||||
wallpapers.append(.init(
|
||||
name: name,
|
||||
lightImage: wpImg, darkImage: darkImg,
|
||||
selectedImageDataLight: selectedLight, selectedImageDataDark: selectedDark
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
// sort them
|
||||
if let sortedWallpapers = CarPlayManager.getCarPlayWallpaperNames() {
|
||||
wallpapers = wallpapers.reorder(by: sortedWallpapers)
|
||||
}
|
||||
} catch {
|
||||
UIApplication.shared.alert(body: NSLocalizedString("Failed to fetch CarPlay wallpapers", comment: "") + ":\n\(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
|
||||
if newItem == nil { return }
|
||||
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
|
||||
}
|
||||
selectedItem = nil
|
||||
withAnimation {
|
||||
didChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WallpaperImageView: View {
|
||||
var img: UIImage
|
||||
|
||||
var body: some View {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.aspectRatio(1.35, contentMode: .fill)
|
||||
.cornerRadius(8)
|
||||
.clipped()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// ExploreView.swift
|
||||
// Pocket Poster
|
||||
//
|
||||
// Created by lemin on 7/6/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CachedAsyncImage
|
||||
|
||||
let MIN_SIZE: CGFloat = 165
|
||||
let CORNER_RADIUS: CGFloat = 12
|
||||
|
||||
struct ExploreView: View {
|
||||
@ObservedObject var cowabungaAPI = CowabungaAPI.shared
|
||||
|
||||
// lazyvgrid
|
||||
@State private var gridItemLayout = [GridItem(.adaptive(minimum: MIN_SIZE))]
|
||||
@State private var wallpapers: [DownloadableWallpaper] = []
|
||||
|
||||
@State var wallpaperTypeSelected = 0
|
||||
@State var wallpaperTypeShown = DownloadableWallpaper.WallpaperType.custom
|
||||
|
||||
@State var filterType: FilterType = .random
|
||||
@State var searchTerm: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
PullToRefresh(coordinateSpaceName: "pullToRefresh") {
|
||||
// refresh
|
||||
wallpapers.removeAll()
|
||||
Haptic.shared.play(.light)
|
||||
//URLCache.imageCache.removeAllCachedResponses()
|
||||
loadWallpapers()
|
||||
}
|
||||
VStack {
|
||||
Picker("", selection: $wallpaperTypeSelected) {
|
||||
Text("Custom").tag(0)
|
||||
Text("Apple").tag(1)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
if wallpapers.isEmpty {
|
||||
ProgressView()
|
||||
.scaleEffect(1.75)
|
||||
.navigationTitle("Explore")
|
||||
} else {
|
||||
LazyVGrid(columns: gridItemLayout) {
|
||||
ForEach(wallpapers) { wallpaper in
|
||||
if searchTerm == "" || wallpaper.name.lowercased().contains(searchTerm.lowercased()) || (wallpaper.authors ?? "").lowercased().contains(searchTerm.lowercased()) {
|
||||
Button(action: {
|
||||
DownloadManager.shared.startTendiesDownload(for: cowabungaAPI.getDownloadURLForWallpaper(wallpaper: wallpaper))
|
||||
}) {
|
||||
VStack(spacing: 0) {
|
||||
ZStack {
|
||||
Color.gray.opacity(0.4)
|
||||
if wallpaper.previewIsGif() {
|
||||
GIFImage(url: cowabungaAPI.getPreviewURLForWallpaper(wallpaper: wallpaper), animate: true, loop: true)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
CachedAsyncImage(url: cowabungaAPI.getPreviewURLForWallpaper(wallpaper: wallpaper), urlCache: .imageCache) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
} placeholder: {
|
||||
Color.gray
|
||||
.frame(height: MIN_SIZE)
|
||||
}
|
||||
}
|
||||
}
|
||||
.cornerRadius(CORNER_RADIUS, corners: .topLeft)
|
||||
.cornerRadius(CORNER_RADIUS, corners: .topRight)
|
||||
HStack {
|
||||
VStack(spacing: 4) {
|
||||
HStack {
|
||||
Text(wallpaper.name)
|
||||
.foregroundStyle(Color(uiColor: .label))
|
||||
.minimumScaleFactor(0.5)
|
||||
Spacer()
|
||||
}
|
||||
if let authors = wallpaper.authors {
|
||||
HStack {
|
||||
Text(authors)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.minimumScaleFactor(0.5)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
Image(systemName: "arrow.down.circle")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.frame(height: 58)
|
||||
}
|
||||
}
|
||||
.frame(minWidth: MIN_SIZE)
|
||||
.background(Color(uiColor: .secondarySystemBackground))
|
||||
.cornerRadius(CORNER_RADIUS)
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchTerm)
|
||||
.coordinateSpace(name: "pullToRefresh")
|
||||
.navigationTitle("Explore")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
showFilterChangerPopup()
|
||||
}) {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadWallpapers()
|
||||
}
|
||||
.onChange(of: wallpaperTypeSelected) { newValue in
|
||||
let map = [0: DownloadableWallpaper.WallpaperType.custom, 1: .apple, 2: .template]
|
||||
wallpaperTypeShown = map[newValue]!
|
||||
|
||||
wallpapers.removeAll()
|
||||
loadWallpapers()
|
||||
}
|
||||
}
|
||||
|
||||
func loadWallpapers() {
|
||||
Task {
|
||||
do {
|
||||
wallpapers = try await cowabungaAPI.fetchWallpapers(type: wallpaperTypeShown)
|
||||
wallpapers = cowabungaAPI.filterWallpapers(wallpapers: wallpapers, filterType: filterType)
|
||||
} catch {
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Failed to fetch wallpapers", comment: ""), body: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showFilterChangerPopup() {
|
||||
// create and configure alert controller
|
||||
let alert = UIAlertController(title: NSLocalizedString("Filter Wallpapers", comment: ""), message: "", preferredStyle: .actionSheet)
|
||||
|
||||
// create the actions
|
||||
for type in FilterType.allCases {
|
||||
let newAction = UIAlertAction(title: type.rawValue, style: .default) { (action) in
|
||||
// apply the filter type
|
||||
filterType = type
|
||||
wallpapers.removeAll()
|
||||
loadWallpapers()
|
||||
}
|
||||
if filterType == type {
|
||||
// add a check mark
|
||||
newAction.setValue(true, forKey: "checked")
|
||||
}
|
||||
alert.addAction(newAction)
|
||||
}
|
||||
|
||||
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { (action) in
|
||||
// cancels the action
|
||||
}
|
||||
|
||||
// add the actions
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
let view: UIView = UIApplication.shared.windows.first!.rootViewController!.view
|
||||
// present popover for iPads
|
||||
alert.popoverPresentationController?.sourceView = view // prevents crashing on iPads
|
||||
alert.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY, width: 0, height: 0) // show up at center bottom on iPads
|
||||
|
||||
// present the alert
|
||||
UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
struct PullToRefresh: View {
|
||||
var coordinateSpaceName: String
|
||||
var onRefresh: ()->Void
|
||||
|
||||
@State var needRefresh: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
|
||||
Spacer()
|
||||
.onAppear {
|
||||
needRefresh = true
|
||||
}
|
||||
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
|
||||
Spacer()
|
||||
.onAppear {
|
||||
if needRefresh {
|
||||
needRefresh = false
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
if needRefresh {
|
||||
ProgressView()
|
||||
.scaleEffect(1.75)
|
||||
.onAppear {
|
||||
Haptic.shared.play(.light)
|
||||
}
|
||||
} else {
|
||||
Text("")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}.padding(.top, -50)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,16 @@ struct RootView: View {
|
||||
.tabItem {
|
||||
Label("Videos", systemImage: "camera")
|
||||
}
|
||||
if CarPlayManager.supportsCarPlay() {
|
||||
CarPlayView()
|
||||
.tabItem {
|
||||
Label("CarPlay", systemImage: "car")
|
||||
}
|
||||
}
|
||||
ExploreView()
|
||||
.tabItem {
|
||||
Label("Explore", systemImage: "safari")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,22 +20,30 @@ 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))
|
||||
if CarPlayManager.supportsCarPlay() {
|
||||
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: {
|
||||
if !FileManager.default.fileExists(atPath: SymHandler.getPosterBoardHashURL().path()) {
|
||||
// don't show the alert because it is already there
|
||||
UIApplication.shared.confirmAlert(title: NSLocalizedString("Waiting for app hash...", comment: ""), body: NSLocalizedString("Connect your device to Nugget and click the \"Pocket Poster Helper\" button.", comment: ""), confirmTitle: NSLocalizedString("Cancel", comment: ""), onOK: {
|
||||
cancelWaitForHash()
|
||||
}, noCancel: true)
|
||||
}
|
||||
startWaitForHash()
|
||||
}) {
|
||||
Text("Detect")
|
||||
}
|
||||
.foregroundStyle(.green)
|
||||
.onChange(of: checkingForHash) {
|
||||
.onChange(of: checkingForHash) { _ in
|
||||
if !checkingForHash {
|
||||
// hide ui alert
|
||||
UIApplication.shared.dismissAlert(animated: true)
|
||||
@@ -77,6 +85,17 @@ struct SettingsView: View {
|
||||
Label("Clear App Cache", systemImage: "trash.circle")
|
||||
}
|
||||
.foregroundStyle(.red)
|
||||
|
||||
Button(action: {
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
UserDefaults.standard.set(nil, forKey: "ActiveCarPlayWallpapers")
|
||||
try? FileManager.default.removeItem(at: CarPlayManager.getCarPlayPhotosURL())
|
||||
Haptic.shared.notify(.success)
|
||||
UIApplication.shared.alert(title: NSLocalizedString("CarPlay Applied Wallpapers Successfully Cleared!", comment: ""), body: "")
|
||||
}) {
|
||||
Label("Reset CarPlay Applied Wallpapers", systemImage: "trash.circle")
|
||||
}
|
||||
.foregroundStyle(.red)
|
||||
} header: {
|
||||
Label("Actions", systemImage: "gear")
|
||||
}
|
||||
@@ -134,7 +153,7 @@ struct SettingsView: View {
|
||||
func startWaitForHash() {
|
||||
checkingForHash = true
|
||||
hashCheckTask = Task {
|
||||
let filePath = SymHandler.getLCDocumentsDirectory().appendingPathComponent("NuggetAppHash")
|
||||
let filePath = SymHandler.getPosterBoardHashURL()
|
||||
while !FileManager.default.fileExists(atPath: filePath.path()) {
|
||||
try? await Task.sleep(nanoseconds: 500_000_000) // Sleep 0.5s
|
||||
try Task.checkCancellation()
|
||||
@@ -144,12 +163,17 @@ struct SettingsView: View {
|
||||
let contents = try String(contentsOf: filePath)
|
||||
try? FileManager.default.removeItem(at: filePath)
|
||||
await MainActor.run {
|
||||
let items = contents.split(separator: "\n")
|
||||
if let pb = items.first {
|
||||
pbHash = String(pb)
|
||||
pbHash = contents
|
||||
}
|
||||
// check for carplay hash
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
let carplayPath = SymHandler.getCarPlayHashURL()
|
||||
if FileManager.default.fileExists(atPath: carplayPath.path()) {
|
||||
let carplayContents = try String(contentsOf: carplayPath)
|
||||
try? FileManager.default.removeItem(at: carplayPath)
|
||||
await MainActor.run {
|
||||
cpHash = carplayContents
|
||||
}
|
||||
if items.count >= 2 {
|
||||
cpHash = String(items[1])
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
+8
-2
@@ -1,5 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
CONFIG="Release"
|
||||
|
||||
if [ "$1" == "-d" ]; then
|
||||
CONFIG="Debug"
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
@@ -15,13 +21,13 @@ cd build
|
||||
|
||||
xcodebuild -project "$WORKING_LOCATION/$APPLICATION_NAME.xcodeproj" \
|
||||
-scheme "$APPLICATION_NAME" \
|
||||
-configuration Release \
|
||||
-configuration "$CONFIG" \
|
||||
-derivedDataPath "$WORKING_LOCATION/build/DerivedDataApp" \
|
||||
-destination 'generic/platform=iOS' \
|
||||
clean build \
|
||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO"
|
||||
|
||||
DD_APP_PATH="$WORKING_LOCATION/build/DerivedDataApp/Build/Products/Release-iphoneos/$APPLICATION_NAME.app"
|
||||
DD_APP_PATH="$WORKING_LOCATION/build/DerivedDataApp/Build/Products/$CONFIG-iphoneos/$APPLICATION_NAME.app"
|
||||
TARGET_APP="$WORKING_LOCATION/build/$APPLICATION_NAME.app"
|
||||
cp -r "$DD_APP_PATH" "$TARGET_APP"
|
||||
|
||||
|
||||
Reference in new issue
Block a user