FTStore now holds both halves of the product:
ios/ the swift app — the fork of claration/Feather, unchanged apart
from its new home
web/ FTStore Admin, formerly its own repository. serves the
dashboard at /admin, the ftstore.json feed and IPA downloads
app-repo.json and Images/ stay at the repo root on purpose: the former is
a published AltStore source whose raw.githubusercontent URL is what
installed copies resolve updates through, so moving it would break every
subscriber.
The admin server is renamed throughout: package ftstore-admin, images
ghcr.io/<owner>/ftstore-admin-{app,worker}, postgres role/db ftstore,
session cookie ftstore_session, temp dir /tmp/ftstore, backup type
ftstore-settings, outbound user-agent FTStoreAdmin. backups written under the old
name still import, and compose is pinned to name: ftstore so the
volume prefix does not follow the directory name.
apply-rebrand.sh gains a relocation step: upstream's tree is rooted at
the repo root while ours lives in ios/, so a merge drops Feather's files
next to ios/ and the script folds them in before renaming. it never
touches web/. still idempotent.
release tags are namespaced — v* for the app, web-v* for the server —
so a server release no longer triggers a macOS build or rewrites
app-repo.json. workflows are prefixed ios-/web- and path-filtered.
74 lines
2.6 KiB
Swift
74 lines
2.6 KiB
Swift
//
|
||
// UIApplication+open.swift
|
||
// Feather
|
||
//
|
||
// Created by samara on 21.04.2025.
|
||
//
|
||
|
||
import UIKit.UIApplication
|
||
|
||
extension UIApplication {
|
||
/// Opens an app with an identifier
|
||
/// - Parameter identifier: Application identifier
|
||
static public func openApp(with identifier: String) {
|
||
let classNameBase64 = "TFNBcHBsaWNhdGlvbldvcmtzcGFjZQ==" // LSApplicationWorkspace
|
||
let defaultSelectorBase64 = "ZGVmYXVsdFdvcmtzcGFjZQ==" // defaultWorkspace
|
||
let openSelectorBase64 = "b3BlbkFwcGxpY2F0aW9uV2l0aEJ1bmRsZUlEOg==" // openApplicationWithBundleID:
|
||
|
||
guard
|
||
let classNameData = Data(base64Encoded: classNameBase64),
|
||
let defaultSelectorData = Data(base64Encoded: defaultSelectorBase64),
|
||
let openSelectorData = Data(base64Encoded: openSelectorBase64),
|
||
let className = String(data: classNameData, encoding: .utf8),
|
||
let defaultSelector = String(data: defaultSelectorData, encoding: .utf8),
|
||
let openSelector = String(data: openSelectorData, encoding: .utf8)
|
||
else {
|
||
return
|
||
}
|
||
|
||
guard
|
||
let workspaceClass = NSClassFromString(className) as? NSObject.Type,
|
||
let workspace = workspaceClass.perform(NSSelectorFromString(defaultSelector))?.takeUnretainedValue()
|
||
else {
|
||
return
|
||
}
|
||
|
||
_ = workspace.perform(NSSelectorFromString(openSelector), with: identifier)
|
||
}
|
||
|
||
/// Returns install progress for a bundle identifier (0.0 – 1.0)
|
||
/// - Parameters:
|
||
/// - identifier: Bundle identifier
|
||
/// - synchronous: Whether the call should block
|
||
/// - Returns: Progress value if available
|
||
static public func installProgress(
|
||
for identifier: String,
|
||
makeSynchronous synchronous: Bool = true
|
||
) -> Double? {
|
||
|
||
let classNameBase64 = "TFNBcHBsaWNhdGlvbldvcmtzcGFjZQ==" // LSApplicationWorkspace
|
||
let defaultSelectorBase64 = "ZGVmYXVsdFdvcmtzcGFjZQ==" // defaultWorkspace
|
||
let progressSelectorBase64 = "aW5zdGFsbFByb2dyZXNzRm9yQnVuZGxlSUQ6bWFrZVN5bmNocm9ub3VzOg==" // installProgressForBundleID:makeSynchronous:
|
||
|
||
guard
|
||
let className = String(data: Data(base64Encoded: classNameBase64)!, encoding: .utf8),
|
||
let defaultSelector = String(data: Data(base64Encoded: defaultSelectorBase64)!, encoding: .utf8),
|
||
let progressSelector = String(data: Data(base64Encoded: progressSelectorBase64)!, encoding: .utf8),
|
||
let workspaceClass = NSClassFromString(className) as? NSObject.Type,
|
||
let workspace = workspaceClass.perform(NSSelectorFromString(defaultSelector))?.takeUnretainedValue()
|
||
else { return nil }
|
||
|
||
let result = workspace.perform(
|
||
NSSelectorFromString(progressSelector),
|
||
with: identifier,
|
||
with: synchronous
|
||
)?.takeUnretainedValue()
|
||
|
||
if let number = result as? Progress {
|
||
return number.fractionCompleted
|
||
}
|
||
|
||
return nil
|
||
}
|
||
}
|