import Combine import SwiftUI import UIKit /// What the window holds: the native interface, with ipx's page a button away. /// /// The page is not a fallback here, it is the rest of the app. Settings, the admin page, the /// directory and OPML have never been native and are not meant to be (#5), and an item's show /// notes are still HTML (#4). Both live behind the same web view that used to be the whole /// interface, so nothing has been lost by putting a list in front of it. final class RootViewController: UIViewController { private let api = API() private var store: LibraryStore! private var cookies: CookieBridge! private var playback: Playback! private var page: WebViewController! private var host: UIHostingController! private var watching: AnyCancellable? override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black // The page is built first and kept: it owns the web view whose cookies authenticate // everything, including the API client and the player. Signing in happens there. page = WebViewController() page.loadViewIfNeeded() cookies = page.cookieBridge playback = page.player store = LibraryStore(api: api) let root = LibraryView( store: store, playback: playback, play: { [weak self] entry, file in self?.play(entry, file) }, openPage: { [weak self] in self?.showPage() }) .environment(\.api, api) host = UIHostingController(rootView: AnyView(root)) addChild(host) host.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(host.view) NSLayoutConstraint.activate([ host.view.topAnchor.constraint(equalTo: view.topAnchor), host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), ]) host.didMove(toParent: self) // preferredColorScheme inside a hosting controller does not reliably reach the whole // hierarchy -- the list stayed light against a dark account. Overriding the style on the // controller does, and it carries to the page presented over it as well. watching = store.$appearance.sink { [weak self] scheme in let style: UIUserInterfaceStyle switch scheme { case .some(.dark): style = .dark case .some(.light): style = .light default: style = .unspecified } self?.overrideUserInterfaceStyle = style } } /// Plays a row through the same Playback the lock screen and the car drive. private func play(_ entry: IPX.Entry, _ asked: IPX.Enclosure?) { guard let file = asked ?? entry.playable, let url = ServerSettings.url("/media/\(file.id)") else { return } let feed = store.feed(entry.feedId) playback.load(.init( url: url, feedId: entry.feedId, guid: entry.guid, title: entry.title ?? "", feedTitle: feed?.name ?? entry.feedId, artwork: entry.image ?? feed?.image, position: entry.position, duration: entry.duration)) // Where it starts is the entry's own position, since there is no page here to decide it. if entry.position > 5 { playback.seek(to: Double(entry.position)) } playback.play() } private func showPage() { guard page.presentingViewController == nil else { return } let nav = UINavigationController(rootViewController: page) page.navigationItem.leftBarButtonItem = UIBarButtonItem( systemItem: .done, primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) // The page may have changed read state or subscriptions while it was open. Task { await self?.store.refreshFeeds(); self?.store.reload() } }) present(nav, animated: true) } }