295 lines
10 KiB
Swift
295 lines
10 KiB
Swift
import UIKit
|
|
import WidgetKit
|
|
|
|
@MainActor
|
|
final class AppContext {
|
|
let core: GotchaCore
|
|
lazy var notifications = NotificationCoordinator(context: self)
|
|
private let window: UIWindow
|
|
private(set) var tabs = UITabBarController()
|
|
private(set) var navigationControllers: [UINavigationController] = []
|
|
|
|
init(window: UIWindow) {
|
|
self.window = window
|
|
let storageDirectory = FileManager.default.containerURL(
|
|
forSecurityApplicationGroupIdentifier: "group.de.rfc1437.gotcha"
|
|
)?.path
|
|
core = GotchaCore(storageDirectory: storageDirectory)
|
|
applyAppearance()
|
|
}
|
|
|
|
func makeRootController() -> UIViewController {
|
|
rebuildTabs(preservingHomeStack: false)
|
|
return tabs
|
|
}
|
|
|
|
func applyPrimaryDestinations() {
|
|
rebuildTabs(preservingHomeStack: true)
|
|
}
|
|
|
|
private func rebuildTabs(preservingHomeStack: Bool) {
|
|
let destinations = core.settings().primaryDestinations
|
|
let home = preservingHomeStack ? navigationControllers.first : nil
|
|
let roots = destinations.map(destinationController)
|
|
let items = [("Home", "house", "house.fill")] + destinations.map(destinationItem)
|
|
let destinationNavigations = zip(roots, items.dropFirst()).map { root, item in
|
|
let navigation = UINavigationController(rootViewController: root)
|
|
navigation.tabBarItem = UITabBarItem(
|
|
title: item.0,
|
|
image: UIImage(systemName: item.1),
|
|
selectedImage: UIImage(systemName: item.2)
|
|
)
|
|
return navigation
|
|
}
|
|
let homeNavigation = home ?? UINavigationController(
|
|
rootViewController: HomeViewController(context: self)
|
|
)
|
|
homeNavigation.tabBarItem = UITabBarItem(
|
|
title: items[0].0,
|
|
image: UIImage(systemName: items[0].1),
|
|
selectedImage: UIImage(systemName: items[0].2)
|
|
)
|
|
navigationControllers = [homeNavigation] + destinationNavigations
|
|
tabs.viewControllers = navigationControllers
|
|
(homeNavigation.viewControllers.first as? HomeViewController)?.refreshPrimaryDestinations()
|
|
}
|
|
|
|
func showStartupErrorIfNeeded() {
|
|
guard let message = core.startupError() else { return }
|
|
tabs.present(errorAlert(message), animated: true)
|
|
}
|
|
|
|
func selectServer(index: UInt32) throws {
|
|
try core.selectServer(index: index)
|
|
reloadAfterServerChange()
|
|
}
|
|
|
|
func reloadAfterServerChange() {
|
|
rebuildTabs(preservingHomeStack: false)
|
|
WidgetCenter.shared.reloadAllTimelines()
|
|
}
|
|
|
|
@discardableResult
|
|
func route(widgetURL: URL) -> Bool {
|
|
guard widgetURL.scheme == "gotcha" else { return false }
|
|
switch widgetURL.host {
|
|
case "home":
|
|
tabs.selectedIndex = 0
|
|
navigationControllers[0].popToRootViewController(animated: false)
|
|
case "pulls":
|
|
if let index = primaryDestinations.firstIndex(of: .pullRequests).map({ $0 + 1 }) {
|
|
tabs.selectedIndex = index
|
|
navigationControllers[index].popToRootViewController(animated: false)
|
|
} else {
|
|
tabs.selectedIndex = 0
|
|
navigationControllers[0].pushViewController(
|
|
PullsViewController(context: self),
|
|
animated: false
|
|
)
|
|
}
|
|
default: return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
var primaryDestinations: [PrimaryDestination] {
|
|
core.settings().primaryDestinations
|
|
}
|
|
|
|
var secondaryDestinations: [PrimaryDestination] {
|
|
PrimaryDestination.allCases.filter { !primaryDestinations.contains($0) }
|
|
}
|
|
|
|
func show(_ destination: PrimaryDestination, from navigation: UINavigationController?) {
|
|
if let index = primaryDestinations.firstIndex(of: destination).map({ $0 + 1 }) {
|
|
tabs.selectedIndex = index
|
|
navigationControllers[index].popToRootViewController(animated: false)
|
|
} else {
|
|
navigation?.pushViewController(destinationController(destination), animated: true)
|
|
}
|
|
}
|
|
|
|
func didAddServer(index: UInt32) throws {
|
|
try selectServer(index: index)
|
|
}
|
|
|
|
func applyAppearance() {
|
|
switch core.settings().appearance {
|
|
case 1: window.overrideUserInterfaceStyle = .light
|
|
case 2: window.overrideUserInterfaceStyle = .dark
|
|
default: window.overrideUserInterfaceStyle = .unspecified
|
|
}
|
|
}
|
|
|
|
func route(_ activity: ActivityRow) {
|
|
route(
|
|
serverId: nil,
|
|
target: activity.target,
|
|
owner: activity.owner,
|
|
repository: activity.repository,
|
|
number: activity.number,
|
|
sha: activity.sha
|
|
)
|
|
}
|
|
|
|
func route(_ notification: NotificationRow) {
|
|
route(
|
|
serverId: notification.serverId,
|
|
target: notification.target,
|
|
owner: notification.owner,
|
|
repository: notification.repository,
|
|
number: notification.number,
|
|
sha: notification.sha
|
|
)
|
|
}
|
|
|
|
func route(notificationUserInfo userInfo: [AnyHashable: Any]) {
|
|
let target: ActivityTargetKind
|
|
switch userInfo["target"] as? String {
|
|
case "repository": target = .repository
|
|
case "issue": target = .issue
|
|
case "pull": target = .pullRequest
|
|
case "commit": target = .commit
|
|
default: target = .none
|
|
}
|
|
route(
|
|
serverId: userInfo["serverId"] as? String,
|
|
target: target,
|
|
owner: userInfo["owner"] as? String ?? "",
|
|
repository: userInfo["repository"] as? String ?? "",
|
|
number: (userInfo["number"] as? NSNumber)?.int64Value ?? 0,
|
|
sha: userInfo["sha"] as? String ?? ""
|
|
)
|
|
}
|
|
|
|
private func route(
|
|
serverId: String?,
|
|
target: ActivityTargetKind,
|
|
owner: String,
|
|
repository: String,
|
|
number: Int64,
|
|
sha: String
|
|
) {
|
|
if let serverId {
|
|
guard let index = core.servers().firstIndex(where: { $0.id == serverId }) else {
|
|
tabs.present(errorAlert("That notification's server is no longer configured."), animated: true)
|
|
return
|
|
}
|
|
if core.activeServerIndex() != UInt32(index) {
|
|
do {
|
|
try selectServer(index: UInt32(index))
|
|
} catch {
|
|
tabs.present(errorAlert(errorMessage(error)), animated: true)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
let navigation = navigationControllers[0]
|
|
tabs.selectedIndex = 0
|
|
switch target {
|
|
case .repository:
|
|
navigation.pushViewController(
|
|
IssuesViewController(
|
|
context: self,
|
|
owner: owner,
|
|
repository: repository
|
|
),
|
|
animated: true
|
|
)
|
|
case .issue:
|
|
navigation.pushViewController(
|
|
IssueViewController(
|
|
context: self,
|
|
owner: owner,
|
|
repository: repository,
|
|
number: number
|
|
),
|
|
animated: true
|
|
)
|
|
case .pullRequest:
|
|
navigation.pushViewController(
|
|
PullViewController(
|
|
context: self,
|
|
owner: owner,
|
|
repository: repository,
|
|
number: number
|
|
),
|
|
animated: true
|
|
)
|
|
case .commit:
|
|
navigation.pushViewController(
|
|
FilesViewController(
|
|
context: self,
|
|
owner: owner,
|
|
repository: repository,
|
|
sha: sha
|
|
),
|
|
animated: true
|
|
)
|
|
case .none:
|
|
break
|
|
}
|
|
}
|
|
|
|
func symbol(_ name: String) -> UIImage? {
|
|
UIImage(systemName: name)
|
|
}
|
|
|
|
private func repositoryRoot(mode: RepositoryPane) -> UIViewController {
|
|
if core.activeServerIndex() == nil {
|
|
return ServersViewController(context: self)
|
|
}
|
|
return RepositoriesViewController(context: self, mode: mode)
|
|
}
|
|
|
|
private func destinationController(_ destination: PrimaryDestination) -> UIViewController {
|
|
switch destination {
|
|
case .issues: return repositoryRoot(mode: .issues)
|
|
case .repositories: return repositoryRoot(mode: .commits)
|
|
case .pullRequests: return PullsViewController(context: self)
|
|
case .milestones: return repositoryRoot(mode: .milestones)
|
|
case .actions: return repositoryRoot(mode: .actions)
|
|
case .serverActivity: return ServerActivityViewController(context: self)
|
|
}
|
|
}
|
|
|
|
private func destinationItem(_ destination: PrimaryDestination) -> (String, String, String) {
|
|
switch destination {
|
|
case .issues: return ("Issues", "exclamationmark.circle", "exclamationmark.circle.fill")
|
|
case .repositories: return ("Repos", "books.vertical", "books.vertical.fill")
|
|
case .pullRequests: return ("PRs", "arrow.triangle.pull", "arrow.triangle.pull")
|
|
case .milestones: return ("Milestones", "flag", "flag.fill")
|
|
case .actions: return ("Actions", "play.square.stack", "play.square.stack.fill")
|
|
case .serverActivity: return ("Activity", "person.3", "person.3.fill")
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PrimaryDestination: CaseIterable {
|
|
public static var allCases: [PrimaryDestination] {
|
|
[.issues, .repositories, .pullRequests, .milestones, .actions]
|
|
}
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .issues: return "Issues"
|
|
case .repositories: return "Repositories"
|
|
case .pullRequests: return "Pull Requests"
|
|
case .milestones: return "Milestones"
|
|
case .actions: return "Actions"
|
|
case .serverActivity: return "Server Activity"
|
|
}
|
|
}
|
|
|
|
var symbolName: String {
|
|
switch self {
|
|
case .issues: return "exclamationmark.circle"
|
|
case .repositories: return "books.vertical"
|
|
case .pullRequests: return "arrow.triangle.pull"
|
|
case .milestones: return "flag"
|
|
case .actions: return "play.square.stack"
|
|
case .serverActivity: return "person.3"
|
|
}
|
|
}
|
|
}
|