Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 106 additions & 133 deletions Feather/FeatherApp.swift → Feather/Entry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,140 +10,21 @@ import Nuke
import IDeviceSwift
import OSLog

@main
struct FeatherApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

let heartbeat = HeartbeatManager.shared

@StateObject var downloadManager = DownloadManager.shared
let storage = Storage.shared

var body: some Scene {
WindowGroup {
VStack {
DownloadHeaderView(downloadManager: downloadManager)
.transition(.move(edge: .top).combined(with: .opacity))
VariedTabbarView()
.environment(\.managedObjectContext, storage.context)
.onOpenURL(perform: _handleURL)
.transition(.move(edge: .top).combined(with: .opacity))
}
.animation(.smooth, value: downloadManager.manualDownloads.description)
.onReceive(NotificationCenter.default.publisher(for: .heartbeatInvalidHost)) { _ in
DispatchQueue.main.async {
UIAlertController.showAlertWithOk(
title: "InvalidHostID",
message: .localized("Your pairing file is invalid and is incompatible with your device, please import a valid pairing file.")
)
}
}
// dear god help me
.onAppear {
if let style = UIUserInterfaceStyle(rawValue: UserDefaults.standard.integer(forKey: "Feather.userInterfaceStyle")) {
UIApplication.topViewController()?.view.window?.overrideUserInterfaceStyle = style
}

UIApplication.topViewController()?.view.window?.tintColor = UIColor(Color(hex: UserDefaults.standard.string(forKey: "Feather.userTintColor") ?? "#848ef9"))
}
}
}

private func _handleURL(_ url: URL) {
if url.scheme == "feather" {
/// feather://import-certificate?p12=<base64>&mobileprovision=<base64>&password=<base64>
if url.host == "import-certificate" {
guard
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems
else {
return
}

func queryValue(_ name: String) -> String? {
queryItems.first(where: { $0.name == name })?.value?.removingPercentEncoding
}

guard
let p12Base64 = queryValue("p12"),
let provisionBase64 = queryValue("mobileprovision"),
let passwordBase64 = queryValue("password"),
let passwordData = Data(base64Encoded: passwordBase64),
let password = String(data: passwordData, encoding: .utf8)
else {
return
}

let generator = UINotificationFeedbackGenerator()
generator.prepare()

guard
let p12URL = FileManager.default.decodeAndWrite(base64: p12Base64, pathComponent: ".p12"),
let provisionURL = FileManager.default.decodeAndWrite(base64: provisionBase64, pathComponent: ".mobileprovision"),
FR.checkPasswordForCertificate(for: p12URL, with: password, using: provisionURL)
else {
generator.notificationOccurred(.error)
return
}

FR.handleCertificateFiles(
p12URL: p12URL,
provisionURL: provisionURL,
p12Password: password
) { error in
if let error = error {
UIAlertController.showAlertWithOk(title: .localized("Error"), message: error.localizedDescription)
} else {
generator.notificationOccurred(.success)
}
}

return
}
/// feather://export-certificate?callback_template=<template>
/// ?callback_template=: This is how we callback to the application requesting the certificate, this will be a url scheme
/// example: livecontainer%3A%2F%2Fcertificate%3Fcert%3D%24%28BASE64_CERT%29%26password%3D%24%28PASSWORD%29
/// decoded: livecontainer://certificate?cert=$(BASE64_CERT)&password=$(PASSWORD)
/// $(BASE64_CERT) and $(PASSWORD) must be presenting in the callback template so we can replace them with the proper content
if url.host == "export-certificate" {
guard
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
else {
return
}

let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let callbackTemplate = queryItems["callback_template"]?.removingPercentEncoding else { return }

FR.exportCertificateAndOpenUrl(using: callbackTemplate)
}
/// feather://source/<url>
if let fullPath = url.validatedScheme(after: "/source/") {
FR.handleSource(fullPath) { }
}
/// feather://install/<url.ipa>
if
let fullPath = url.validatedScheme(after: "/install/"),
let downloadURL = URL(string: fullPath)
{
_ = DownloadManager.shared.startDownload(from: downloadURL)
}
} else {
if url.pathExtension == "ipa" || url.pathExtension == "tipa" {
if FileManager.default.isFileFromFileProvider(at: url) {
guard url.startAccessingSecurityScopedResource() else { return }
FR.handlePackageFile(url) { _ in }
} else {
FR.handlePackageFile(url) { _ in }
}

return
}
}
@main enum Entry {
static func main() {
let _ = Storage.shared
let _ = DownloadManager.shared
let _ = HeartbeatManager.shared

let delegate = AppDelegate()
UIApplication.shared.delegate = delegate
_ = UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))
}
}

class AppDelegate: NSObject, UIApplicationDelegate {
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
Expand All @@ -152,6 +33,17 @@ class AppDelegate: NSObject, UIApplicationDelegate {
_createDocumentsDirectories()
ResetView.clearWorkCache()
_addDefaultCertificates()

let tc = TabBarController()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = tc
window?.makeKeyAndVisible()

DispatchQueue.main.async {
self.window!.tintColor = UIColor(Color(hex: UserDefaults.standard.string(forKey: "Feather.userTintColor") ?? "#848ef9"))
self.window!.overrideUserInterfaceStyle = UIUserInterfaceStyle(rawValue: UserDefaults.standard.integer(forKey: "Feather.userInterfaceStyle")) ?? .unspecified
}

return true
}

Expand All @@ -164,10 +56,13 @@ class AppDelegate: NSObject, UIApplicationDelegate {
config.urlCache = nil
return DataLoader(configuration: config)
}()
let dataCache = try? DataCache(name: "thewonderofyou.Feather.datacache") // disk cache
let imageCache = Nuke.ImageCache() // memory cache

let dataCache = try? DataCache(name: "\(Bundle.main.bundleIdentifier!).datacache")
let imageCache = Nuke.ImageCache()

dataCache?.sizeLimit = 500 * 1024 * 1024
imageCache.costLimit = 100 * 1024 * 1024

$0.dataCache = dataCache
$0.imageCache = imageCache
$0.dataLoader = dataLoader
Expand Down Expand Up @@ -243,5 +138,83 @@ class AppDelegate: NSObject, UIApplicationDelegate {
Logger.misc.error("Failed to list signing-assets: \(error)")
}
}

func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
if url.scheme == "feather" {
/// feather://import-certificate?p12=<base64>&mobileprovision=<base64>&password=<base64>
if url.host == "import-certificate" {
guard
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems
else {
return false
}

func queryValue(_ name: String) -> String? {
queryItems.first(where: { $0.name == name })?.value?.removingPercentEncoding
}

guard
let p12Base64 = queryValue("p12"),
let provisionBase64 = queryValue("mobileprovision"),
let passwordBase64 = queryValue("password"),
let passwordData = Data(base64Encoded: passwordBase64),
let password = String(data: passwordData, encoding: .utf8)
else {
return false
}

let generator = UINotificationFeedbackGenerator()
generator.prepare()

guard
let p12URL = FileManager.default.decodeAndWrite(base64: p12Base64, pathComponent: ".p12"),
let provisionURL = FileManager.default.decodeAndWrite(base64: provisionBase64, pathComponent: ".mobileprovision"),
FR.checkPasswordForCertificate(for: p12URL, with: password, using: provisionURL)
else {
generator.notificationOccurred(.error)
return false
}

FR.handleCertificateFiles(
p12URL: p12URL,
provisionURL: provisionURL,
p12Password: password
) { error in
if let error = error {
UIAlertController.showAlertWithOk(title: .localized("Error"), message: error.localizedDescription)
} else {
generator.notificationOccurred(.success)
}
}

return true
}
/// feather://source/<url>
if let fullPath = url.validatedScheme(after: "/source/") {
FR.handleSource(fullPath) { }
}
/// feather://install/<url.ipa>
if
let fullPath = url.validatedScheme(after: "/install/"),
let downloadURL = URL(string: fullPath)
{
_ = DownloadManager.shared.startDownload(from: downloadURL)
}
} else {
if url.pathExtension == "ipa" || url.pathExtension == "tipa" {
if FileManager.default.isFileFromFileProvider(at: url) {
guard url.startAccessingSecurityScopedResource() else { return false }
FR.handlePackageFile(url) { _ in }
} else {
FR.handlePackageFile(url) { _ in }
}

return true
}
}

return false
}

}
53 changes: 53 additions & 0 deletions Feather/Views/Settings/SettingsViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// SettingsViewController.swift
// Feather
//
// Created by samsam on 5/10/26.
//

import UIKit

class SettingsViewController: UITableViewController {
init() {
super.init(style: .insetGrouped)
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
self.title = .localized("Settings")
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "SettingsCell")
}
}

extension SettingsViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
1
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "SettingsCell",
for: indexPath
)

cell.accessoryType = .disclosureIndicator

var content = cell.defaultContentConfiguration()
content.text = "hai"
cell.contentConfiguration = content

return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
Loading