Files
stemify/ios/Stemify/UI/Nav.swift
T

104 lines
3.8 KiB
Swift

import SwiftUI
enum Tab: String, CaseIterable, Hashable {
case Library, Songs, Albums, Artists, Genres, Playlists, Search, Downloads, Settings
static let defaultLayout: [Tab] = [.Library, .Playlists, .Search, .Downloads, .Settings]
/// iOS folds anything past five tabs into "More", so the bar holds at most this many.
static let maxVisible = 5
/// Parses the saved comma list; Settings is always kept so the app can't lock itself out.
static func layout(_ raw: String) -> [Tab] {
var out: [Tab] = []
for part in raw.split(separator: ",") {
if let t = Tab(rawValue: String(part)), !out.contains(t) { out.append(t) }
}
if out.isEmpty { return defaultLayout }
if !out.contains(.Settings) { out = Array(out.prefix(maxVisible - 1)) + [.Settings] }
return Array(out.prefix(maxVisible))
}
static func encode(_ tabs: [Tab]) -> String { tabs.map(\.rawValue).joined(separator: ",") }
var icon: String {
switch self {
case .Library: "square.stack.fill"
case .Songs: "music.note"
case .Albums: "square.stack"
case .Artists: "music.mic"
case .Genres: "guitars.fill"
case .Playlists: "music.note.list"
case .Search: "magnifyingglass"
case .Downloads: "arrow.down.circle.fill"
case .Settings: "gearshape.fill"
}
}
}
enum Route: Hashable {
case Artists, Albums, Songs, Playlists, Genres, Downloads, DownloadQueue, TabBar, WebRadio
case Eq, EqFilters(profileId: String), EqFilterPage(profileId: String, filterId: String)
case Servers, ServerList, Server(String), StemServer, Quality, Storage, Appearance, NowPlayingOptions, StemEditor, About
case GenrePage(String), ArtistPage(Artist), AlbumPage(Album), PlaylistPage(Playlist), StemPlaylistPage(String)
}
/// Tabs, their stacks and the Now Playing sheet: the things any screen can ask of the shell.
@MainActor
@Observable
final class Ui {
var tab: Tab = .Library
var paths: [Tab: [Route]] = [:]
var nowPlayingOpen = false {
didSet { overlays.level = nowPlayingOpen ? 1 : 0 }
}
let overlays: Overlays
init(overlays: Overlays) { self.overlays = overlays }
func path(_ t: Tab) -> Binding<[Route]> {
Binding(get: { self.paths[t] ?? [] }, set: { self.paths[t] = $0 })
}
func push(_ r: Route) { paths[tab, default: []].append(r) }
func pop() { if paths[tab]?.isEmpty == false { paths[tab]?.removeLast() } }
/// Tapping the selected tab again pops it to its root.
func selectTab(_ t: Tab) {
if t == tab { paths[t] = [] } else { tab = t }
}
/// The tabs currently in the bar, in order.
var tabs: [Tab] { Tab.layout(AppGraph.shared.settings.tabLayout) }
/// Where library pages open: Library if it's in the bar, else the first tab that is.
private var home: Tab { tabs.contains(.Library) ? .Library : (tabs.first ?? .Settings) }
/// Downloads as its own tab, or pushed onto the current one when it's hidden.
func openDownloads() {
if tabs.contains(.Downloads) { selectTab(.Downloads) } else { push(.Downloads) }
}
/// Falls back to the first tab when the selected one was just removed from the bar.
func ensureValidTab() {
if !tabs.contains(tab) { tab = tabs.first ?? .Settings }
}
func openNowPlaying() { nowPlayingOpen = true }
/// Switches to the Library tab and pushes [r] there.
func showInLibrary(_ r: Route) {
nowPlayingOpen = false
let t = home
tab = t
paths[t, default: []].append(r)
}
/// Switches to Settings and opens [r] (the Equalizer) there.
func showInSettings(_ r: Route) {
nowPlayingOpen = false
tab = .Settings
if paths[.Settings]?.last != r { paths[.Settings, default: []].append(r) }
}
}