A native feed list and item list (#3)
The lists are native now, and the page is a button in the toolbar: it is still where signing in happens, and it is still the whole of settings, the admin page, the directory and OPML, which were never going to be rewritten. LibraryStore holds a page of items and asks for the next, because the sorting, filtering and searching are the server's work and ten thousand items have no business being in memory to be sorted here. Read and pinned are set locally and sent after, as the page's readWrites map does and for the same reason: a list asked for before the write lands answers with the old state, which put the dot back on an item just read. The interface follows the account's light or dark rather than the phone's, and defaults to dark when nobody has chosen, because that is what ipx's own theme.ts does. Following the system instead put a light list in front of a dark page. preferredColorScheme was not enough on its own -- inside a hosting controller it did not reach the hierarchy -- so the style is overridden on the controller, which also carries to the page presented over it. The sidebar had to be broken into sub-views: the whole list in one expression was more than the type checker would work through, and it said so rather than compiling it. Tested against the real library, 135 feeds and eleven thousand items, and the playback test now drives the native row rather than the page's button. Thirteen pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
95
ios/Sources/RootViewController.swift
Normal file
95
ios/Sources/RootViewController.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
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<AnyView>!
|
||||
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 in self?.play(entry) },
|
||||
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) {
|
||||
guard let file = 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user