diff --git a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift index f29bb6f7..e0ec1e73 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift @@ -50,18 +50,18 @@ final class MainCoordinator: NavigationCoordinatable { Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn)) Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut)) Notifications[.processDeepLink].subscribe(self, selector: #selector(processDeepLink(_:))) - Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeServerCurrentURI(_:))) + Notifications[.didChangeCurrentServerURL].subscribe(self, selector: #selector(didChangeCurrentServerURL(_:))) } @objc func didSignIn() { - logger.info("Received `didSignIn` from SwiftfinNotificationCenter.") + logger.info("Signed in") root(\.mainTab) } @objc func didSignOut() { - logger.info("Received `didSignOut` from SwiftfinNotificationCenter.") + logger.info("Signed out") root(\.serverList) } @@ -80,13 +80,12 @@ final class MainCoordinator: NavigationCoordinatable { } @objc - func didChangeServerCurrentURI(_ notification: Notification) { -// guard let newCurrentServerState = notification.object as? SwiftfinStore.State.Server -// else { fatalError("Need to have new current login state server") } -// guard SessionManager.main.currentLogin != nil else { return } -// if newCurrentServerState.id == SessionManager.main.currentLogin.server.id { -// SessionManager.main.signInUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user) -// } + func didChangeCurrentServerURL(_ notification: Notification) { + + guard Container.userSession().authenticated else { return } + + Container.userSession.reset() + Notifications[.didSignIn].post() } func makeMainTab() -> MainTabCoordinator { diff --git a/Shared/Coordinators/ServerDetailCoordinator.swift b/Shared/Coordinators/ServerDetailCoordinator.swift deleted file mode 100644 index 33e8222b..00000000 --- a/Shared/Coordinators/ServerDetailCoordinator.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Foundation -import Stinsen -import SwiftUI - -final class ServerDetailCoordinator: NavigationCoordinatable { - - let stack = NavigationStack(initial: \ServerDetailCoordinator.start) - - @Root - var start = makeStart - - let viewModel: ServerDetailViewModel - - init(viewModel: ServerDetailViewModel) { - self.viewModel = viewModel - } - - @ViewBuilder - func makeStart() -> some View { - ServerDetailView(viewModel: viewModel) - } -} diff --git a/Shared/Coordinators/ServerListCoordinator.swift b/Shared/Coordinators/ServerListCoordinator.swift index 7c50ffa2..e16ac17a 100644 --- a/Shared/Coordinators/ServerListCoordinator.swift +++ b/Shared/Coordinators/ServerListCoordinator.swift @@ -28,8 +28,8 @@ final class ServerListCoordinator: NavigationCoordinatable { ConnectToServerCoodinator() } - func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator { - UserListCoordinator(viewModel: .init(server: server)) + func makeUserList(server: ServerState) -> UserListCoordinator { + UserListCoordinator(server: server) } func makeBasicAppSettings() -> NavigationViewCoordinator { diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index b42597fb..c10097c5 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -107,7 +107,7 @@ final class SettingsCoordinator: NavigationCoordinatable { @ViewBuilder func makeServerDetail(server: ServerState) -> some View { - ServerDetailView(viewModel: .init(server: server)) + ServerDetailView(server: server) } #if DEBUG @@ -155,7 +155,7 @@ final class SettingsCoordinator: NavigationCoordinatable { func makeServerDetail(server: ServerState) -> NavigationViewCoordinator { NavigationViewCoordinator( BasicNavigationViewCoordinator { - ServerDetailView(viewModel: .init(server: server)) + ServerDetailView(server: server) } ) } diff --git a/Shared/Coordinators/UserListCoordinator.swift b/Shared/Coordinators/UserListCoordinator.swift index b59e694c..31b339a1 100644 --- a/Shared/Coordinators/UserListCoordinator.swift +++ b/Shared/Coordinators/UserListCoordinator.swift @@ -21,22 +21,22 @@ final class UserListCoordinator: NavigationCoordinatable { @Route(.push) var serverDetail = makeServerDetail - let viewModel: UserListViewModel + let serverState: ServerState - init(viewModel: UserListViewModel) { - self.viewModel = viewModel + init(server: ServerState) { + self.serverState = server } func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator { UserSignInCoordinator(viewModel: .init(server: server)) } - func makeServerDetail(server: SwiftfinStore.State.Server) -> ServerDetailCoordinator { - ServerDetailCoordinator(viewModel: .init(server: server)) + func makeServerDetail(server: SwiftfinStore.State.Server) -> some View { + ServerDetailView(server: server) } @ViewBuilder func makeStart() -> some View { - UserListView(viewModel: viewModel) + UserListView(server: serverState) } } diff --git a/Shared/Services/SwiftfinNotifications.swift b/Shared/Services/SwiftfinNotifications.swift index 6a2ecec4..f97807d5 100644 --- a/Shared/Services/SwiftfinNotifications.swift +++ b/Shared/Services/SwiftfinNotifications.swift @@ -73,7 +73,7 @@ extension Notifications.Key { static let didSignOut = NotificationKey("didSignOut") static let processDeepLink = NotificationKey("processDeepLink") static let didPurge = NotificationKey("didPurge") - static let didChangeServerCurrentURI = NotificationKey("didChangeCurrentLoginURI") + static let didChangeCurrentServerURL = NotificationKey("didChangeCurrentServerURL") static let didSendStopReport = NotificationKey("didSendStopReport") static let didRequestGlobalRefresh = NotificationKey("didRequestGlobalRefresh") diff --git a/Shared/ViewModels/ServerDetailViewModel.swift b/Shared/ViewModels/ServerDetailViewModel.swift index 3c543dc8..2a54caf8 100644 --- a/Shared/ViewModels/ServerDetailViewModel.swift +++ b/Shared/ViewModels/ServerDetailViewModel.swift @@ -6,6 +6,7 @@ // Copyright (c) 2024 Jellyfin & Jellyfin Contributors // +import CoreStore import Foundation import JellyfinAPI @@ -18,16 +19,36 @@ class ServerDetailViewModel: ViewModel { self.server = server } - func setServerCurrentURI(uri: String) { + func setCurrentServerURL(to url: URL) { -// SessionManager.main.setServerCurrentURI(server: server, uri: uri) -// .sink { c in -// print(c) -// } receiveValue: { newServerState in -// self.server = newServerState -// -// Notifications[.didChangeServerCurrentURI].post(object: newServerState) -// } -// .store(in: &cancellables) + guard let storedServer = try? SwiftfinStore.dataStack.fetchOne( + From(), + [Where("id == %@", server.id)] + ) else { + logger.error("Unable to find server") + return + } + + guard storedServer.urls.contains(url) else { + logger.error("Server did not have matching URL") + return + } + + let transaction = SwiftfinStore.dataStack.beginUnsafe() + + guard let editServer = transaction.edit(storedServer) else { + logger.error("Unable to create edit server instance") + return + } + + editServer.currentURL = url + + do { + try transaction.commitAndWait() + + Notifications[.didChangeCurrentServerURL].post(object: editServer.state) + } catch { + logger.error("Unable to edit server") + } } } diff --git a/Shared/ViewModels/UserListViewModel.swift b/Shared/ViewModels/UserListViewModel.swift index c7fb08e0..6e1e012b 100644 --- a/Shared/ViewModels/UserListViewModel.swift +++ b/Shared/ViewModels/UserListViewModel.swift @@ -18,32 +18,36 @@ class UserListViewModel: ViewModel { @Published private(set) var users: [UserState] = [] + @Published + private(set) var server: ServerState - let client: JellyfinClient - let server: ServerState - - init(server: ServerState) { - self.client = JellyfinClient( + var client: JellyfinClient { + JellyfinClient( configuration: .swiftfinConfiguration(url: server.currentURL), sessionDelegate: URLSessionProxyDelegate() ) + } + + init(server: ServerState) { self.server = server super.init() -// Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeCurrentLoginURI(_:))) - } - - @objc - func didChangeCurrentLoginURI(_ notification: Notification) { -// guard let newServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new state server") } -// self.server = newServerState + Notifications[.didChangeCurrentServerURL] + .publisher + .sink { [weak self] notification in + guard let serverState = notification.object as? SwiftfinStore.State.Server else { + return + } + self?.server = serverState + } + .store(in: &cancellables) } func fetchUsers() { guard let storedServer = try? SwiftfinStore.dataStack.fetchOne( - From(), - Where("id == %@", server.id) + From(), + Where("id == %@", server.id) ) else { fatalError("No stored server associated with given state server?") } @@ -58,7 +62,23 @@ class UserListViewModel: ViewModel { Notifications[.didSignIn].post() } - func remove(user: SwiftfinStore.State.User) { - fetchUsers() + func remove(user: UserState) { + guard let storedUser = try? SwiftfinStore.dataStack.fetchOne( + From(), + [Where("id == %@", user.id)] + ) else { + logger.error("Unable to find user to delete") + return + } + + let transaction = SwiftfinStore.dataStack.beginUnsafe() + transaction.delete(storedUser) + + do { + try transaction.commitAndWait() + fetchUsers() + } catch { + logger.error("Unable to delete user") + } } } diff --git a/Shared/ViewModels/ViewModel.swift b/Shared/ViewModels/ViewModel.swift index 5da77098..79e81947 100644 --- a/Shared/ViewModels/ViewModel.swift +++ b/Shared/ViewModels/ViewModel.swift @@ -28,5 +28,13 @@ class ViewModel: ObservableObject { var cancellables = Set() - init() {} + private var userSessionResolverCancellable: AnyCancellable? + + init() { + userSessionResolverCancellable = Notifications[.didChangeCurrentServerURL] + .publisher + .sink { [weak self] _ in + self?.$userSession.resolve(reset: .scope) + } + } } diff --git a/Swiftfin tvOS/Components/ServerButton.swift b/Swiftfin tvOS/Components/ServerButton.swift index c937d2f1..ef7f7653 100644 --- a/Swiftfin tvOS/Components/ServerButton.swift +++ b/Swiftfin tvOS/Components/ServerButton.swift @@ -35,7 +35,9 @@ struct ServerButton: View { Spacer() } + .padding(10) } + .buttonStyle(.card) } } diff --git a/Swiftfin tvOS/Views/ServerDetailView.swift b/Swiftfin tvOS/Views/ServerDetailView.swift index 124bb970..3360e560 100644 --- a/Swiftfin tvOS/Views/ServerDetailView.swift +++ b/Swiftfin tvOS/Views/ServerDetailView.swift @@ -10,8 +10,12 @@ import SwiftUI struct ServerDetailView: View { - @ObservedObject - var viewModel: ServerDetailViewModel + @StateObject + private var viewModel: ServerDetailViewModel + + init(server: ServerState) { + self._viewModel = StateObject(wrappedValue: ServerDetailViewModel(server: server)) + } var body: some View { SplitFormWindowView() diff --git a/Swiftfin tvOS/Views/ServerListView.swift b/Swiftfin tvOS/Views/ServerListView.swift index 53d63ce3..119be167 100644 --- a/Swiftfin tvOS/Views/ServerListView.swift +++ b/Swiftfin tvOS/Views/ServerListView.swift @@ -6,7 +6,7 @@ // Copyright (c) 2024 Jellyfin & Jellyfin Contributors // -import CollectionView +import CollectionVGrid import SwiftUI struct ServerListView: View { @@ -22,22 +22,23 @@ struct ServerListView: View { @ViewBuilder private var listView: some View { - ScrollView { - LazyVStack { - ForEach(viewModel.servers, id: \.id) { server in - ServerButton(server: server) - .onSelect { - router.route(to: \.userList, server) - } - .onLongPressGesture { - longPressedServer = server - } - .padding(.horizontal, 100) + CollectionVGrid( + viewModel.servers, + layout: .columns( + 1, + insets: EdgeInsets.DefaultEdgeInsets, + itemSpacing: EdgeInsets.defaultEdgePadding, + lineSpacing: EdgeInsets.defaultEdgePadding + ) + ) { server in + ServerButton(server: server) + .onSelect { + router.route(to: \.userList, server) + } + .onLongPressGesture { + longPressedServer = server } - } - .padding(.top, 50) } - .padding(.top, 50) } @ViewBuilder @@ -72,27 +73,32 @@ struct ServerListView: View { } var body: some View { - SplitFormWindowView() - .descriptionView { - VStack { - Image(.jellyfinBlobBlue) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: 400) + HStack { + VStack { + Image(.jellyfinBlobBlue) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 400) - Button { - router.route(to: \.connectToServer) - } label: { - L10n.connect.text - .bold() - .font(.callout) - .frame(width: 400, height: 75) - .background(Color.jellyfinPurple) - } - .buttonStyle(.card) + Button { + router.route(to: \.connectToServer) + } label: { + L10n.connect.text + .bold() + .font(.callout) + .frame(width: 400, height: 75) + .background(Color.jellyfinPurple) } + .buttonStyle(.card) } - .contentView {} + .frame(maxWidth: .infinity) + + innerBody + .frame(maxWidth: .infinity) + } + .onAppear { + viewModel.fetchServers() + } } // var body: some View { diff --git a/Swiftfin tvOS/Views/UserListView.swift b/Swiftfin tvOS/Views/UserListView.swift index 08bfe838..a8a537a2 100644 --- a/Swiftfin tvOS/Views/UserListView.swift +++ b/Swiftfin tvOS/Views/UserListView.swift @@ -6,7 +6,7 @@ // Copyright (c) 2024 Jellyfin & Jellyfin Contributors // -import CollectionView +import CollectionVGrid import Factory import JellyfinAPI import SwiftUI @@ -16,15 +16,27 @@ struct UserListView: View { @EnvironmentObject private var router: UserListCoordinator.Router - @ObservedObject - var viewModel: UserListViewModel - @State private var longPressedUser: SwiftfinStore.State.User? + @StateObject + private var viewModel: UserListViewModel + + init(server: ServerState) { + self._viewModel = StateObject(wrappedValue: UserListViewModel(server: server)) + } + @ViewBuilder private var listView: some View { - CollectionView(items: viewModel.users) { _, user, _ in + CollectionVGrid( + viewModel.users, + layout: .minWidth( + 250, + insets: EdgeInsets.DefaultEdgeInsets, + itemSpacing: EdgeInsets.defaultEdgePadding, + lineSpacing: EdgeInsets.defaultEdgePadding + ) + ) { user in UserProfileButton(user: user) .onSelect { viewModel.signIn(user: user) @@ -33,16 +45,6 @@ struct UserListView: View { longPressedUser = user } } - .layout { _, layoutEnvironment in - .grid( - layoutEnvironment: layoutEnvironment, - layoutMode: .adaptive(withMinItemSize: 250), - itemSpacing: 20, - lineSpacing: 20, - sectionInsets: .init(top: 20, leading: 20, bottom: 20, trailing: 20) - ) - } - .padding(50) } @ViewBuilder diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index dbe3313a..43a523bc 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -247,8 +247,6 @@ E11CEB8D28999B4A003E74C7 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8C28999B4A003E74C7 /* Font.swift */; }; E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8F28999D84003E74C7 /* EpisodeItemView.swift */; }; E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB9328999D9E003E74C7 /* EpisodeItemContentView.swift */; }; - E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; - E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; E11E374D293E7EC9009EF240 /* ItemFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D842902933F87500D1041A /* ItemFields.swift */; }; E11E374E293E7F08009EF240 /* MediaSourceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D8428E2933F2D900D1041A /* MediaSourceInfo.swift */; }; E11E376D293E9CC1009EF240 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18A8E8428D60D0000333B9A /* VideoPlayerCoordinator.swift */; }; @@ -993,7 +991,6 @@ E11CEB8C28999B4A003E74C7 /* Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = ""; }; E11CEB8F28999D84003E74C7 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; E11CEB9328999D9E003E74C7 /* EpisodeItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemContentView.swift; sourceTree = ""; }; - E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = ""; }; E122A9122788EAAD0060FA63 /* MediaStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStream.swift; sourceTree = ""; }; E12376AD2A33D680001F5B44 /* AboutViewCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewCard.swift; sourceTree = ""; }; E12376AF2A33D6AE001F5B44 /* AboutViewCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewCard.swift; sourceTree = ""; }; @@ -1934,7 +1931,6 @@ E1A1528F28FD23D600600579 /* PlaybackSettingsCoordinator.swift */, E18CE0B828A2322D0092E7F1 /* QuickConnectCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, - E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */, E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */, 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */, @@ -3302,7 +3298,6 @@ E1E1643E28BB074000323B0A /* SelectorView.swift in Sources */, E1A1529128FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */, E187A60529AD2E25008387E6 /* StepperView.swift in Sources */, - E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */, E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */, @@ -3702,7 +3697,6 @@ E1921B7428E61914003A5238 /* SpecialFeatureHStack.swift in Sources */, C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */, E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */, - E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */, 6264E88C273850380081A12A /* Strings.swift in Sources */, C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */, E1BDF31729525F0400CC0294 /* AdvancedActionButton.swift in Sources */, @@ -4258,7 +4252,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -4274,7 +4268,7 @@ ); MARKETING_VERSION = 1.0.0; OTHER_CFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -4298,7 +4292,7 @@ CURRENT_PROJECT_VERSION = 78; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -4314,7 +4308,7 @@ ); MARKETING_VERSION = 1.0.0; OTHER_CFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; diff --git a/Swiftfin/Views/ServerDetailView.swift b/Swiftfin/Views/ServerDetailView.swift index 3cc23efd..ae8fc765 100644 --- a/Swiftfin/Views/ServerDetailView.swift +++ b/Swiftfin/Views/ServerDetailView.swift @@ -6,58 +6,55 @@ // Copyright (c) 2024 Jellyfin & Jellyfin Contributors // +import Factory import SwiftUI struct ServerDetailView: View { - @ObservedObject - var viewModel: ServerDetailViewModel - @State - private var currentServerURI: String + private var currentServerURL: URL - init(viewModel: ServerDetailViewModel) { - self.viewModel = viewModel - self._currentServerURI = State(initialValue: viewModel.server.currentURL.absoluteString) + @StateObject + private var viewModel: ServerDetailViewModel + + init(server: ServerState) { + self._viewModel = StateObject(wrappedValue: ServerDetailViewModel(server: server)) + self._currentServerURL = State(initialValue: server.currentURL) } var body: some View { Form { Section { - HStack { - L10n.name.text - Spacer() - Text(viewModel.server.name) - .foregroundColor(.secondary) - } - Picker(L10n.url, selection: $currentServerURI) { + TextPairView( + leading: L10n.name, + trailing: viewModel.server.name + ) + + Picker(L10n.url, selection: $currentServerURL) { ForEach(viewModel.server.urls.sorted(using: \.absoluteString)) { url in Text(url.absoluteString) .tag(url) .foregroundColor(.secondary) } - .onChange(of: currentServerURI) { _ in + .onChange(of: currentServerURL) { _ in // TODO: change server url + viewModel.setCurrentServerURL(to: currentServerURL) } } - HStack { - L10n.version.text - Spacer() - Text(viewModel.server.version) - .foregroundColor(.secondary) - } + TextPairView( + leading: L10n.version, + trailing: viewModel.server.version + ) - HStack { - L10n.operatingSystem.text - Spacer() - Text(viewModel.server.os) - .foregroundColor(.secondary) - } + TextPairView( + leading: L10n.operatingSystem, + trailing: viewModel.server.os + ) } } .navigationBarTitleDisplayMode(.inline) - .navigationTitle(L10n.serverDetails.text) + .navigationTitle(L10n.server) } } diff --git a/Swiftfin/Views/UserListView.swift b/Swiftfin/Views/UserListView.swift index c6951fc0..4d08f0f0 100644 --- a/Swiftfin/Views/UserListView.swift +++ b/Swiftfin/Views/UserListView.swift @@ -14,8 +14,12 @@ struct UserListView: View { @EnvironmentObject private var router: UserListCoordinator.Router - @ObservedObject - var viewModel: UserListViewModel + @StateObject + private var viewModel: UserListViewModel + + init(server: ServerState) { + self._viewModel = StateObject(wrappedValue: UserListViewModel(server: server)) + } private var noUserView: some View { VStack {