Files
Felitendo ff0d3108fe feat: merge the web dashboard into the monorepo as ftstore admin
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.
2026-08-03 10:25:03 +02:00

74 lines
2.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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
}
}