Started working on multiple server configuration
This commit is contained in:
@@ -38,15 +38,6 @@ struct API
|
|||||||
{
|
{
|
||||||
let baseURL: URL
|
let baseURL: URL
|
||||||
|
|
||||||
static func fromSettings() -> Self? {
|
|
||||||
let settings = Settings.fromDefaults()
|
|
||||||
|
|
||||||
guard let baseURL = settings.serverURL.flatMap({ URL(string: $0) })
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
return API(baseURL: baseURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(baseURL: URL) {
|
init(baseURL: URL) {
|
||||||
self.baseURL = baseURL
|
self.baseURL = baseURL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,15 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Server: Identifiable
|
struct Server: Identifiable, Codable
|
||||||
{
|
{
|
||||||
let serviceName: String?
|
let serviceName: String?
|
||||||
let baseURL: URL
|
let baseURL: URL
|
||||||
|
|
||||||
var id: String { baseURL.absoluteString }
|
var id: String { baseURL.absoluteString }
|
||||||
|
|
||||||
|
var api: API { API(baseURL: baseURL) }
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
if let serviceName {
|
if let serviceName {
|
||||||
return serviceName.queueCubeServiceName
|
return serviceName.queueCubeServiceName
|
||||||
|
|||||||
@@ -9,15 +9,29 @@ import Foundation
|
|||||||
|
|
||||||
struct Settings
|
struct Settings
|
||||||
{
|
{
|
||||||
var serverURL: String?
|
var configuredServers: [Server]
|
||||||
|
|
||||||
|
var isConfigured: Bool {
|
||||||
|
!configuredServers.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
static func fromDefaults() -> Settings {
|
static func fromDefaults() -> Settings {
|
||||||
let serverURL = UserDefaults.standard.string(forKey: Keys.serverURL.rawValue)
|
let configuredServers: [Server] = {
|
||||||
return Settings(serverURL: serverURL)
|
guard let configuredServersData = UserDefaults.standard.data(forKey: Keys.configuredServers.rawValue)
|
||||||
|
else { return [] }
|
||||||
|
|
||||||
|
guard let configuredServers = try? PropertyListDecoder().decode([Server].self, from: configuredServersData)
|
||||||
|
else { return [] }
|
||||||
|
|
||||||
|
return configuredServers
|
||||||
|
}()
|
||||||
|
|
||||||
|
return Settings(configuredServers: configuredServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func save() {
|
func save() {
|
||||||
UserDefaults.standard.set(serverURL, forKey: Keys.serverURL.rawValue)
|
let configuredServersData = try! PropertyListEncoder().encode(configuredServers)
|
||||||
|
UserDefaults.standard.set(configuredServersData, forKey: Keys.configuredServers.rawValue)
|
||||||
NotificationCenter.default.post(name: .settingsChanged, object: nil)
|
NotificationCenter.default.post(name: .settingsChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +39,7 @@ struct Settings
|
|||||||
|
|
||||||
enum Keys: String
|
enum Keys: String
|
||||||
{
|
{
|
||||||
case serverURL
|
case configuredServers
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Server: Codable
|
struct Server: Codable
|
||||||
|
|||||||
@@ -125,6 +125,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"NO_SERVERS_CONFIGURED" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "No Servers Configured"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"NOT_CONFIGURED" : {
|
"NOT_CONFIGURED" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@@ -9,25 +9,26 @@ import SwiftUI
|
|||||||
|
|
||||||
extension LocalizedStringKey
|
extension LocalizedStringKey
|
||||||
{
|
{
|
||||||
static let serverURL = LocalizedStringKey("SERVER_URL")
|
static let serverURL = LocalizedStringKey("SERVER_URL")
|
||||||
static let settings = LocalizedStringKey("SETTINGS")
|
static let settings = LocalizedStringKey("SETTINGS")
|
||||||
static let settings_ = LocalizedStringKey("SETTINGS_ELLIPSES")
|
static let settings_ = LocalizedStringKey("SETTINGS_ELLIPSES")
|
||||||
static let done = LocalizedStringKey("DONE")
|
static let done = LocalizedStringKey("DONE")
|
||||||
static let notConfigured = LocalizedStringKey("NOT_CONFIGURED")
|
static let notConfigured = LocalizedStringKey("NOT_CONFIGURED")
|
||||||
static let add = LocalizedStringKey("ADD")
|
static let add = LocalizedStringKey("ADD")
|
||||||
static let addAnyURL = LocalizedStringKey("ADD_ANY_URL")
|
static let addAnyURL = LocalizedStringKey("ADD_ANY_URL")
|
||||||
static let serverIsOnline = LocalizedStringKey("SERVER_IS_ONLINE")
|
static let serverIsOnline = LocalizedStringKey("SERVER_IS_ONLINE")
|
||||||
static let unableToConnect = LocalizedStringKey("UNABLE_TO_CONNECT")
|
static let unableToConnect = LocalizedStringKey("UNABLE_TO_CONNECT")
|
||||||
static let configuration = LocalizedStringKey("CONFIGURATION")
|
static let configuration = LocalizedStringKey("CONFIGURATION")
|
||||||
static let validating = LocalizedStringKey("VALIDATING")
|
static let validating = LocalizedStringKey("VALIDATING")
|
||||||
static let general = LocalizedStringKey("GENERAL")
|
static let general = LocalizedStringKey("GENERAL")
|
||||||
static let connectionError = LocalizedStringKey("CONNECTION_ERROR")
|
static let connectionError = LocalizedStringKey("CONNECTION_ERROR")
|
||||||
static let playlist = LocalizedStringKey("PLAYLIST")
|
static let playlist = LocalizedStringKey("PLAYLIST")
|
||||||
static let favorites = LocalizedStringKey("FAVORITES")
|
static let favorites = LocalizedStringKey("FAVORITES")
|
||||||
static let servers = LocalizedStringKey("SERVERS")
|
static let servers = LocalizedStringKey("SERVERS")
|
||||||
static let addServer = LocalizedStringKey("ADD_SERVER")
|
static let addServer = LocalizedStringKey("ADD_SERVER")
|
||||||
static let cancel = LocalizedStringKey("CANCEL")
|
static let cancel = LocalizedStringKey("CANCEL")
|
||||||
static let manual = LocalizedStringKey("ENTER_MANUALLY")
|
static let manual = LocalizedStringKey("ENTER_MANUALLY")
|
||||||
static let discovered = LocalizedStringKey("DISCOVERED")
|
static let discovered = LocalizedStringKey("DISCOVERED")
|
||||||
static let findingServers = LocalizedStringKey("FINDING_SERVERS")
|
static let findingServers = LocalizedStringKey("FINDING_SERVERS")
|
||||||
|
static let noServersConfigured = LocalizedStringKey("NO_SERVERS_CONFIGURED")
|
||||||
}
|
}
|
||||||
|
|||||||
57
QueueCube/Views/ContentPlaceholderView.swift
Normal file
57
QueueCube/Views/ContentPlaceholderView.swift
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
//
|
||||||
|
// ContentPlaceholderView.swift
|
||||||
|
// QueueCube
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 6/10/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ContentPlaceholderView<Label, Actions>: View
|
||||||
|
where Label: View, Actions: View
|
||||||
|
{
|
||||||
|
let label: Label
|
||||||
|
let actions: Actions
|
||||||
|
|
||||||
|
init(@ViewBuilder label: () -> Label, @ViewBuilder actions: () -> Actions = { EmptyView() }) {
|
||||||
|
self.label = label()
|
||||||
|
self.actions = actions()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
ContentUnavailableView {
|
||||||
|
label
|
||||||
|
.imageScale(.large)
|
||||||
|
.tint(.secondary)
|
||||||
|
} actions: { actions }
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contentPlaceholderView<Actions>(
|
||||||
|
title: LocalizedStringKey,
|
||||||
|
systemImage: String, @ViewBuilder actions: () -> Actions = { EmptyView() })
|
||||||
|
-> ContentPlaceholderView<AnyView, Actions>
|
||||||
|
{
|
||||||
|
ContentPlaceholderView(label: {
|
||||||
|
AnyView(erasing: VStack(spacing: 16.0) {
|
||||||
|
Image(systemName: systemImage)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
|
||||||
|
.frame(width: 50.0, height: 50.0)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.imageScale(.large)
|
||||||
|
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.bold()
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 14.0)
|
||||||
|
})
|
||||||
|
}, actions: actions)
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ struct ContentView: View
|
|||||||
init() {
|
init() {
|
||||||
self.model = MainViewModel()
|
self.model = MainViewModel()
|
||||||
|
|
||||||
if let api = model.api {
|
if let api = model.selectedServer?.api {
|
||||||
let nowPlayingModel = self.model.nowPlayingViewModel
|
let nowPlayingModel = self.model.nowPlayingViewModel
|
||||||
nowPlayingModel.onPlayPause = { model in
|
nowPlayingModel.onPlayPause = { model in
|
||||||
Task { model.isPlaying ? try await api.pause() : try await api.play() }
|
Task { model.isPlaying ? try await api.pause() : try await api.play() }
|
||||||
@@ -83,7 +83,7 @@ struct ContentView: View
|
|||||||
extension ContentView
|
extension ContentView
|
||||||
{
|
{
|
||||||
private func refresh(_ what: RefreshType) async {
|
private func refresh(_ what: RefreshType) async {
|
||||||
guard let api = model.api else { return }
|
guard let api = model.selectedServer?.api else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if what.contains(.nowPlaying) {
|
if what.contains(.nowPlaying) {
|
||||||
@@ -133,7 +133,7 @@ extension ContentView
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func watchWebsocket() async {
|
private func watchWebsocket() async {
|
||||||
guard let api = model.api else { return }
|
guard let api = model.selectedServer?.api else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
for await streamEvent in try await api.events() {
|
for await streamEvent in try await api.events() {
|
||||||
@@ -175,119 +175,3 @@ extension ContentView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Observable
|
|
||||||
class MainViewModel
|
|
||||||
{
|
|
||||||
var api = API.fromSettings()
|
|
||||||
|
|
||||||
var connectionError: Error? = nil
|
|
||||||
var selectedTab: MainTab = .playlist
|
|
||||||
|
|
||||||
var playlistModel = PlaylistViewModel()
|
|
||||||
var favoritesModel = FavoritesViewModel()
|
|
||||||
var nowPlayingViewModel = NowPlayingViewModel()
|
|
||||||
var addMediaViewModel = AddMediaBarViewModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MainTab: String, CaseIterable {
|
|
||||||
case playlist
|
|
||||||
case favorites
|
|
||||||
case settings
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MainView: View
|
|
||||||
{
|
|
||||||
@State var model: MainViewModel
|
|
||||||
@State var isSettingsVisible: Bool = false
|
|
||||||
|
|
||||||
init(model: MainViewModel) {
|
|
||||||
self.model = model
|
|
||||||
|
|
||||||
Task {
|
|
||||||
let settingsChangedNotifications = NotificationCenter.default.notifications(named: .settingsChanged)
|
|
||||||
.map({ _ in Optional.none })
|
|
||||||
|
|
||||||
for await _ in settingsChangedNotifications {
|
|
||||||
model.api = API.fromSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
let showConfigurationDialog = model.api == nil
|
|
||||||
|
|
||||||
TabView(selection: $model.selectedTab) {
|
|
||||||
Tab(.playlist, systemImage: "list.bullet", value: .playlist) {
|
|
||||||
PlaylistView(model: model.playlistModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
Tab(.favorites, systemImage: "heart.fill", value: .favorites) {
|
|
||||||
FavoritesView(model: model.favoritesModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
Tab(.settings, systemImage: "gear", value: .settings) {
|
|
||||||
SettingsView(onDone: {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#if false
|
|
||||||
VStack {
|
|
||||||
if showConfigurationDialog {
|
|
||||||
ContentPlaceholderView {
|
|
||||||
Image(systemName: "server.rack")
|
|
||||||
Text(.notConfigured)
|
|
||||||
} actions: {
|
|
||||||
Button {
|
|
||||||
isSettingsVisible = true
|
|
||||||
} label: {
|
|
||||||
Text(.settings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if model.connectionError != nil {
|
|
||||||
ContentPlaceholderView {
|
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
|
||||||
Text(.connectionError)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TabView(selection: $model.selectedTab) {
|
|
||||||
}
|
|
||||||
.frame(maxWidth: 640.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
AddMediaBarView(model: model.addMediaViewModel)
|
|
||||||
.layoutPriority(2.0)
|
|
||||||
.disabled(showConfigurationDialog)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $isSettingsVisible) {
|
|
||||||
SettingsView(onDone: { isSettingsVisible = false })
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContentPlaceholderView<Label, Actions>: View
|
|
||||||
where Label: View, Actions: View
|
|
||||||
{
|
|
||||||
let label: Label
|
|
||||||
let actions: Actions
|
|
||||||
|
|
||||||
init(@ViewBuilder label: () -> Label, @ViewBuilder actions: () -> Actions = { EmptyView() }) {
|
|
||||||
self.label = label()
|
|
||||||
self.actions = actions()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
ContentUnavailableView {
|
|
||||||
label
|
|
||||||
.imageScale(.large)
|
|
||||||
} actions: { actions }
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
109
QueueCube/Views/MainView.swift
Normal file
109
QueueCube/Views/MainView.swift
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
//
|
||||||
|
// MainView.swift
|
||||||
|
// QueueCube
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 6/10/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
class MainViewModel
|
||||||
|
{
|
||||||
|
var selectedServer: Server? = nil
|
||||||
|
|
||||||
|
var connectionError: Error? = nil
|
||||||
|
var selectedTab: Tab = .playlist
|
||||||
|
|
||||||
|
var playlistModel = PlaylistViewModel()
|
||||||
|
var favoritesModel = FavoritesViewModel()
|
||||||
|
var nowPlayingViewModel = NowPlayingViewModel()
|
||||||
|
var addMediaViewModel = AddMediaBarViewModel()
|
||||||
|
|
||||||
|
enum Tab: String, CaseIterable
|
||||||
|
{
|
||||||
|
case playlist
|
||||||
|
case favorites
|
||||||
|
case settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MainView: View
|
||||||
|
{
|
||||||
|
@State var model: MainViewModel
|
||||||
|
@State var isSettingsVisible: Bool = false
|
||||||
|
|
||||||
|
init(model: MainViewModel) {
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
// If no servers are configured, make Settings the default tab.
|
||||||
|
if !Settings.fromDefaults().isConfigured {
|
||||||
|
model.selectedTab = .settings
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
let settingsChangedNotifications = NotificationCenter.default.notifications(named: .settingsChanged)
|
||||||
|
.map({ _ in Optional.none })
|
||||||
|
|
||||||
|
for await _ in settingsChangedNotifications {
|
||||||
|
// TODO
|
||||||
|
// model.api = API.fromSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let showConfigurationDialog = model.selectedServer == nil
|
||||||
|
|
||||||
|
TabView(selection: $model.selectedTab) {
|
||||||
|
Tab(.playlist, systemImage: "list.bullet", value: .playlist) {
|
||||||
|
PlaylistView(model: model.playlistModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab(.favorites, systemImage: "heart.fill", value: .favorites) {
|
||||||
|
FavoritesView(model: model.favoritesModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab(.settings, systemImage: "gear", value: .settings) {
|
||||||
|
SettingsView(onDone: {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if false
|
||||||
|
VStack {
|
||||||
|
if showConfigurationDialog {
|
||||||
|
ContentPlaceholderView {
|
||||||
|
Image(systemName: "server.rack")
|
||||||
|
Text(.notConfigured)
|
||||||
|
} actions: {
|
||||||
|
Button {
|
||||||
|
isSettingsVisible = true
|
||||||
|
} label: {
|
||||||
|
Text(.settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if model.connectionError != nil {
|
||||||
|
ContentPlaceholderView {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
Text(.connectionError)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TabView(selection: $model.selectedTab) {
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 640.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
AddMediaBarView(model: model.addMediaViewModel)
|
||||||
|
.layoutPriority(2.0)
|
||||||
|
.disabled(showConfigurationDialog)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $isSettingsVisible) {
|
||||||
|
SettingsView(onDone: { isSettingsVisible = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,119 +1,13 @@
|
|||||||
//
|
//
|
||||||
// SettingsView.swift
|
// AddServerView.swift
|
||||||
// QueueCube
|
// QueueCube
|
||||||
//
|
//
|
||||||
// Created by James Magahern on 5/2/25.
|
// Created by James Magahern on 6/10/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View
|
|
||||||
{
|
|
||||||
let onDone: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
List {
|
|
||||||
NavigationLink(destination: GeneralSettingsView()) {
|
|
||||||
Image(systemName: "gear")
|
|
||||||
Text(.general)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(destination: ServerListSettingsView()) {
|
|
||||||
Image(systemName: "server.rack")
|
|
||||||
Text(.servers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.navigationTitle(.settings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GeneralSettingsView: View
|
|
||||||
{
|
|
||||||
var body: some View {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ServerListSettingsView: View
|
|
||||||
{
|
|
||||||
@State var model = ViewModel()
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
List(model.configuredServers) { server in
|
|
||||||
serverListItem(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigationTitle(.servers)
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
Button {
|
|
||||||
model.isAddServerPresented = true
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.sheet(isPresented: $model.isAddServerPresented) {
|
|
||||||
NavigationView {
|
|
||||||
AddServerView(onAddServer: { model.onAddServer(server: $0) })
|
|
||||||
.navigationTitle(.addServer)
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItemGroup(placement: .cancellationAction) {
|
|
||||||
Button(.cancel) { model.isAddServerPresented = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
func serverListItem(_ server: Server) -> some View {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "hifispeaker.fill")
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(server.displayName)
|
|
||||||
.lineLimit(1)
|
|
||||||
.bold()
|
|
||||||
|
|
||||||
Text(server.baseURL.absoluteString)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Types
|
|
||||||
|
|
||||||
@Observable
|
|
||||||
class ViewModel
|
|
||||||
{
|
|
||||||
var configuredServers: [Server]
|
|
||||||
var isAddServerPresented = false
|
|
||||||
|
|
||||||
init() {
|
|
||||||
self.configuredServers = []
|
|
||||||
}
|
|
||||||
|
|
||||||
func onAddServer(server: Server) {
|
|
||||||
isAddServerPresented = false
|
|
||||||
configuredServers.append(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AddServerView: View
|
struct AddServerView: View
|
||||||
{
|
{
|
||||||
let onAddServer: (Server) -> Void
|
let onAddServer: (Server) -> Void
|
||||||
@@ -240,8 +134,6 @@ struct AddServerView: View
|
|||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
setNeedsValidation()
|
setNeedsValidation()
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
observeForValidation()
|
observeForValidation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,12 +167,7 @@ struct AddServerView: View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveSettings() {
|
|
||||||
Settings(serverURL: self.serverURL)
|
|
||||||
.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Types
|
// MARK: - Types
|
||||||
|
|
||||||
enum ValidationState
|
enum ValidationState
|
||||||
16
QueueCube/Views/Settings View/GeneralSettingsView.swift
Normal file
16
QueueCube/Views/Settings View/GeneralSettingsView.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// GeneralSettingsView.swift
|
||||||
|
// QueueCube
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 6/10/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct GeneralSettingsView: View
|
||||||
|
{
|
||||||
|
var body: some View {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
94
QueueCube/Views/Settings View/ServerListSettingsView.swift
Normal file
94
QueueCube/Views/Settings View/ServerListSettingsView.swift
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
//
|
||||||
|
// ServerListSettingsView.swift
|
||||||
|
// QueueCube
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 6/10/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ServerListSettingsView: View
|
||||||
|
{
|
||||||
|
@State var model = ViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
if model.configuredServers.isEmpty {
|
||||||
|
contentPlaceholderView(title: .noServersConfigured, systemImage: "server.rack") {
|
||||||
|
Button {
|
||||||
|
model.isAddServerPresented = true
|
||||||
|
} label: {
|
||||||
|
Text(.addServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Form {
|
||||||
|
List(model.configuredServers) { server in
|
||||||
|
serverListItem(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigationTitle(.servers)
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
Button {
|
||||||
|
model.isAddServerPresented = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet(isPresented: $model.isAddServerPresented) {
|
||||||
|
NavigationView {
|
||||||
|
AddServerView(onAddServer: { model.onAddServer(server: $0) })
|
||||||
|
.navigationTitle(.addServer)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .cancellationAction) {
|
||||||
|
Button(.cancel) { model.isAddServerPresented = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func serverListItem(_ server: Server) -> some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "hifispeaker.fill")
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(server.displayName)
|
||||||
|
.lineLimit(1)
|
||||||
|
.bold()
|
||||||
|
|
||||||
|
Text(server.baseURL.absoluteString)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Types
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
class ViewModel
|
||||||
|
{
|
||||||
|
var configuredServers: [Server]
|
||||||
|
var isAddServerPresented = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.configuredServers = []
|
||||||
|
}
|
||||||
|
|
||||||
|
func onAddServer(server: Server) {
|
||||||
|
isAddServerPresented = false
|
||||||
|
configuredServers.append(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
QueueCube/Views/Settings View/SettingsView.swift
Normal file
61
QueueCube/Views/Settings View/SettingsView.swift
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// SettingsView.swift
|
||||||
|
// QueueCube
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 5/2/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsView: View
|
||||||
|
{
|
||||||
|
let onDone: () -> Void
|
||||||
|
@State private var navigationPath: [SettingsPage]
|
||||||
|
|
||||||
|
init(onDone: @escaping () -> Void) {
|
||||||
|
self.onDone = onDone
|
||||||
|
self.navigationPath = if !Settings.fromDefaults().isConfigured {
|
||||||
|
// Show server settings if not configured.
|
||||||
|
[ .servers ]
|
||||||
|
} else {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack(path: $navigationPath) {
|
||||||
|
List {
|
||||||
|
NavigationLink(value: SettingsPage.general) {
|
||||||
|
Image(systemName: "gear")
|
||||||
|
Text(.general)
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(value: SettingsPage.servers) {
|
||||||
|
Image(systemName: "server.rack")
|
||||||
|
Text(.servers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationDestination(for: SettingsPage.self, destination: { page in
|
||||||
|
Group {
|
||||||
|
switch page {
|
||||||
|
case .general: GeneralSettingsView()
|
||||||
|
case .servers: ServerListSettingsView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
})
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationTitle(.settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Types
|
||||||
|
|
||||||
|
enum SettingsPage: String, Identifiable
|
||||||
|
{
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
case general
|
||||||
|
case servers
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user