From 97affd198e2717a883fa0b5723a035f329116f5f Mon Sep 17 00:00:00 2001 From: Joe Kribs Date: Thu, 19 Dec 2024 14:30:01 -0700 Subject: [PATCH] [tvOS] Update ConnectToServerView & UserSignInView (#1365) * UserSignInView and ConnectToServerView Cleanup * Public User icon changes, move the Jellyfin 'NavigationBar' to a `View Modifier` for easier re-use. * A better solution * isLoading == isLoading NOT isLoading == true * clean up --------- Co-authored-by: Ethan Pippin --- Shared/Strings/Strings.swift | 4 +- .../Components/SplitLoginWindowView.swift | 73 +++++++++++ .../Modifiers/NavigationBarMenuButton.swift | 34 +++++ Swiftfin tvOS/Extensions/View/View-tvOS.swift | 25 ++++ .../Components/LocalServerButton.swift | 62 +++++++++ .../ConnectToServerView.swift | 122 ++++++------------ .../Views/SelectUserView/SelectUserView.swift | 1 + .../Components/PublicUserButton.swift | 97 ++++++++++++++ .../Components/PublicUserRow.swift | 81 ------------ .../Views/UserSignInView/UserSignInView.swift | 114 ++++++++-------- Swiftfin.xcodeproj/project.pbxproj | 66 +++++++++- 11 files changed, 445 insertions(+), 234 deletions(-) create mode 100644 Swiftfin tvOS/Components/SplitLoginWindowView.swift create mode 100644 Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift create mode 100644 Swiftfin tvOS/Extensions/View/View-tvOS.swift create mode 100644 Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift rename Swiftfin tvOS/Views/{ => ConnectToServerView}/ConnectToServerView.swift (53%) create mode 100644 Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift delete mode 100644 Swiftfin tvOS/Views/UserSignInView/Components/PublicUserRow.swift diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index eb61fe72..472beedb 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -46,8 +46,8 @@ internal enum L10n { internal static let additionalSecurityAccessDescription = L10n.tr("Localizable", "additionalSecurityAccessDescription", fallback: "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.") /// Add Server internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server") - /// Add Trigger - internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add Trigger") + /// Add trigger + internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add trigger") /// Add URL internal static let addURL = L10n.tr("Localizable", "addURL", fallback: "Add URL") /// Add User diff --git a/Swiftfin tvOS/Components/SplitLoginWindowView.swift b/Swiftfin tvOS/Components/SplitLoginWindowView.swift new file mode 100644 index 00000000..f724a38e --- /dev/null +++ b/Swiftfin tvOS/Components/SplitLoginWindowView.swift @@ -0,0 +1,73 @@ +// +// 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 SwiftUI + +struct SplitLoginWindowView: View { + + // MARK: - Loading State + + private let isLoading: Bool + + // MARK: - Content Variables + + private let leadingTitle: String + private let leadingContentView: () -> Leading + private let trailingTitle: String + private let trailingContentView: () -> Trailing + + // MARK: - Body + + var body: some View { + HStack(alignment: .top) { + VStack(alignment: .leading) { + Section(leadingTitle) { + VStack(alignment: .leading) { + leadingContentView() + .eraseToAnyView() + } + .frame(maxWidth: .infinity) + .padding(.vertical) + } + } + + Divider() + .padding(.vertical, 100) + + VStack(alignment: .leading) { + Section(trailingTitle) { + VStack(alignment: .leading) { + trailingContentView() + .eraseToAnyView() + } + .frame(maxWidth: .infinity) + .padding(.vertical) + } + } + } + .navigationBarBranding(isLoading: isLoading) + } +} + +extension SplitLoginWindowView { + + init( + isLoading: Bool = false, + leadingTitle: String, + trailingTitle: String, + @ViewBuilder leadingContentView: @escaping () -> Leading, + @ViewBuilder trailingContentView: @escaping () -> Trailing + ) { + self.isLoading = isLoading + self.leadingTitle = leadingTitle + self.trailingTitle = trailingTitle + self.leadingContentView = leadingContentView + self.trailingContentView = trailingContentView + } +} diff --git a/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift b/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift new file mode 100644 index 00000000..1014c393 --- /dev/null +++ b/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift @@ -0,0 +1,34 @@ +// +// 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 Defaults +import SwiftUI + +struct NavigationBarBrandingModifier: ViewModifier { + + let isLoading: Bool + + func body(content: Content) -> some View { + content + .toolbar { + ToolbarItem(placement: .principal) { + Image(.jellyfinBlobBlue) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 100) + .padding(.bottom, 25) + } + + if isLoading { + ToolbarItem(placement: .topBarTrailing) { + ProgressView() + } + } + } + } +} diff --git a/Swiftfin tvOS/Extensions/View/View-tvOS.swift b/Swiftfin tvOS/Extensions/View/View-tvOS.swift new file mode 100644 index 00000000..6630ee3a --- /dev/null +++ b/Swiftfin tvOS/Extensions/View/View-tvOS.swift @@ -0,0 +1,25 @@ +// +// 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 Defaults +import SwiftUI +import SwiftUIIntrospect + +extension View { + + @ViewBuilder + func navigationBarBranding( + isLoading: Bool = false + ) -> some View { + modifier( + NavigationBarBrandingModifier( + isLoading: isLoading + ) + ) + } +} diff --git a/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift b/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift new file mode 100644 index 00000000..92fca3f6 --- /dev/null +++ b/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift @@ -0,0 +1,62 @@ +// +// 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 Combine +import Defaults +import SwiftUI + +extension ConnectToServerView { + + struct LocalServerButton: View { + + // MARK: - Environment Variables + + @Environment(\.isEnabled) + private var isEnabled: Bool + + // MARK: - Local Server Variables + + private let server: ServerState + private let action: () -> Void + + // MARK: - Initializer + + init(server: ServerState, action: @escaping () -> Void) { + self.server = server + self.action = action + } + + // MARK: - Local Server Button + + var body: some View { + Button(action: action) { + HStack { + VStack(alignment: .leading) { + Text(server.name) + .font(.headline) + .fontWeight(.semibold) + + Text(server.currentURL.absoluteString) + .font(.subheadline) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.body.weight(.regular)) + .foregroundStyle(.secondary) + } + .padding() + } + .disabled(!isEnabled) + .buttonStyle(.card) + .foregroundStyle(.primary) + } + } +} diff --git a/Swiftfin tvOS/Views/ConnectToServerView.swift b/Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift similarity index 53% rename from Swiftfin tvOS/Views/ConnectToServerView.swift rename to Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift index d9c1793e..2fd74630 100644 --- a/Swiftfin tvOS/Views/ConnectToServerView.swift +++ b/Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift @@ -55,82 +55,56 @@ struct ConnectToServerView: View { @ViewBuilder private var connectSection: some View { - Section(L10n.connectToServer) { - TextField(L10n.serverURL, text: $url) - .disableAutocorrection(true) - .textInputAutocapitalization(.never) - .keyboardType(.URL) - .focused($isURLFocused) - } + TextField(L10n.serverURL, text: $url) + .disableAutocorrection(true) + .textInputAutocapitalization(.never) + .keyboardType(.URL) + .focused($isURLFocused) if viewModel.state == .connecting { -// ListRowButton(L10n.cancel) { -// viewModel.send(.cancel) -// } - Button(L10n.cancel) { + ListRowButton(L10n.cancel) { viewModel.send(.cancel) } - .foregroundStyle(.red, .red.opacity(0.2)) + .foregroundStyle(.red, accentColor) + .padding(.vertical) } else { -// ListRowButton(L10n.connect) { -// isURLFocused = false -// viewModel.send(.connect(url)) -// } - Button(L10n.connect) { + ListRowButton(L10n.connect) { isURLFocused = false viewModel.send(.connect(url)) } .disabled(url.isEmpty) .foregroundStyle( accentColor.overlayColor, - accentColor + url.isEmpty ? Color.white.opacity(0.5) : accentColor ) .opacity(url.isEmpty ? 0.5 : 1) + .padding(.vertical) } } - // MARK: - Local Server Button - - private func localServerButton(for server: ServerState) -> some View { - Button { - url = server.currentURL.absoluteString - viewModel.send(.connect(server.currentURL.absoluteString)) - } label: { - HStack { - VStack(alignment: .leading) { - Text(server.name) - .font(.headline) - .fontWeight(.semibold) - - Text(server.currentURL.absoluteString) - .font(.subheadline) - .foregroundColor(.secondary) - } - - Spacer() - - Image(systemName: "chevron.right") - .font(.body.weight(.regular)) - .foregroundColor(.secondary) - } - } - .disabled(viewModel.state == .connecting) - .buttonStyle(.plain) - } - // MARK: - Local Servers Section @ViewBuilder private var localServersSection: some View { - Section(L10n.localServers) { - if viewModel.localServers.isEmpty { - L10n.noLocalServersFound.text - .font(.callout) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity) - } else { - ForEach(viewModel.localServers) { server in - localServerButton(for: server) + if viewModel.localServers.isEmpty { + L10n.noLocalServersFound.text + .font(.callout) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity) + } else { + LazyVGrid( + columns: Array(repeating: GridItem(.flexible()), count: 1), + spacing: 30 + ) { + ForEach(viewModel.localServers, id: \.id) { server in + LocalServerButton(server: server) { + url = server.currentURL.absoluteString + viewModel.send(.connect(server.currentURL.absoluteString)) + } + .environment( + \.isEnabled, + viewModel.state != .connecting && server.currentURL.absoluteString != url + ) } } } @@ -139,34 +113,14 @@ struct ConnectToServerView: View { // MARK: - Body var body: some View { - VStack { - HStack { - Spacer() - - if viewModel.state == .connecting { - ProgressView() - } - } - .frame(height: 100) - .overlay { - Image(.jellyfinBlobBlue) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 100) - .edgePadding() - } - - HStack(alignment: .top) { - VStack(alignment: .leading) { - connectSection - } - - VStack(alignment: .leading) { - localServersSection - } - } - - Spacer() + SplitLoginWindowView( + isLoading: viewModel.state == .connecting, + leadingTitle: L10n.connectToServer, + trailingTitle: L10n.localServers + ) { + connectSection + } trailingContentView: { + localServersSection } .onFirstAppear { isURLFocused = true diff --git a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift index da61e0c8..9e4eda11 100644 --- a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift @@ -289,6 +289,7 @@ struct SelectUserView: View { } } .ignoresSafeArea() + .navigationBarBranding() .onAppear { viewModel.send(.getServers) diff --git a/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift b/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift new file mode 100644 index 00000000..f7ffbbf3 --- /dev/null +++ b/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift @@ -0,0 +1,97 @@ +// +// 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 JellyfinAPI +import SwiftUI + +extension UserSignInView { + + struct PublicUserButton: View { + + // MARK: - Environment Variables + + @Environment(\.isEnabled) + private var isEnabled: Bool + + // MARK: - Public User Variables + + private let user: UserDto + private let client: JellyfinClient + private let action: () -> Void + + // MARK: - Initializer + + init( + user: UserDto, + client: JellyfinClient, + action: @escaping () -> Void + ) { + self.user = user + self.client = client + self.action = action + } + + // MARK: - Fallback Person View + + @ViewBuilder + private var fallbackPersonView: some View { + ZStack { + Color.secondarySystemFill + + RelativeSystemImageView(systemName: "person.fill", ratio: 0.5) + .foregroundStyle(.secondary) + } + .clipShape(.circle) + .aspectRatio(1, contentMode: .fill) + } + + // MARK: - Person View + + @ViewBuilder + private var personView: some View { + ZStack { + Color.clear + + ImageView(user.profileImageSource(client: client, maxWidth: 120)) + .image { image in + image + .posterBorder(ratio: 0.5, of: \.width) + } + .placeholder { _ in + fallbackPersonView + } + .failure { + fallbackPersonView + } + } + } + + // MARK: - Body + + var body: some View { + Button(action: action) { + personView + .aspectRatio(1, contentMode: .fill) + .posterShadow() + .clipShape(.circle) + .frame(width: 150, height: 150) + .hoverEffect(.highlight) + + Text(user.name ?? .emptyDash) + .fontWeight(.semibold) + .foregroundStyle(.primary) + .lineLimit(1) + .padding(.bottom) + } + .buttonBorderShape(.circle) + .buttonStyle(.borderless) + .disabled(!isEnabled) + .foregroundStyle(.primary) + } + } +} diff --git a/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserRow.swift b/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserRow.swift deleted file mode 100644 index 7843b621..00000000 --- a/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserRow.swift +++ /dev/null @@ -1,81 +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 JellyfinAPI -import SwiftUI - -// TODO: change from list to grid button - -extension UserSignInView { - - struct PublicUserRow: View { - - private let user: UserDto - private let client: JellyfinClient - private let action: () -> Void - - init( - user: UserDto, - client: JellyfinClient, - action: @escaping () -> Void - ) { - self.user = user - self.client = client - self.action = action - } - - @ViewBuilder - private var personView: some View { - ZStack { - Color.secondarySystemFill - - RelativeSystemImageView(systemName: "person.fill", ratio: 0.5) - .foregroundStyle(.secondary) - } - .clipShape(.circle) - .aspectRatio(1, contentMode: .fill) - } - - var body: some View { - Button { - action() - } label: { - HStack { - ZStack { - Color.clear - - ImageView(user.profileImageSource(client: client, maxWidth: 120)) - .image { image in - image - .posterBorder(ratio: 0.5, of: \.width) - } - .placeholder { _ in - personView - } - .failure { - personView - } - } - .aspectRatio(1, contentMode: .fill) - .posterShadow() - .clipShape(.circle) - .frame(width: 50, height: 50) - - Text(user.name ?? .emptyDash) - .fontWeight(.semibold) - .foregroundStyle(.primary) - .lineLimit(1) - - Spacer() - } - } - .buttonStyle(.card) - .foregroundStyle(.primary) - } - } -} diff --git a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift index 790e9f63..2821c3f9 100644 --- a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift +++ b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift @@ -13,8 +13,6 @@ import JellyfinAPI import Stinsen import SwiftUI -// TODO: change public users from list to grid - struct UserSignInView: View { // MARK: - Defaults @@ -30,13 +28,16 @@ struct UserSignInView: View { } @FocusState - private var focusedTextField: FocusField? + private var focusedField: FocusField? // MARK: - State & Environment Objects @EnvironmentObject private var router: UserSignInCoordinator.Router + @StateObject + private var focusGuide: FocusGuide = .init() + @StateObject private var viewModel: UserSignInViewModel @@ -69,39 +70,37 @@ struct UserSignInView: View { @ViewBuilder private var signInSection: some View { - Section { - TextField(L10n.username, text: $username) - .autocorrectionDisabled() - .textInputAutocapitalization(.never) - .focused($focusedTextField, equals: .username) + TextField(L10n.username, text: $username) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .focused($focusedField, equals: .username) - SecureField(L10n.password, text: $password) - .focused($focusedTextField, equals: .password) - .onSubmit { - guard username.isNotEmpty else { - return - } - viewModel.send(.signIn(username: username, password: password, policy: .none)) + SecureField(L10n.password, text: $password) + .focused($focusedField, equals: .password) + .onSubmit { + guard username.isNotEmpty else { + return } - } header: { - Text(L10n.signInToServer(viewModel.server.name)) - } + viewModel.send(.signIn(username: username, password: password, policy: .none)) + } if case .signingIn = viewModel.state { - Button(L10n.cancel) { + ListRowButton(L10n.cancel) { viewModel.send(.cancel) } - .foregroundStyle(.red, .red.opacity(0.2)) + .foregroundStyle(.red, accentColor) + .padding(.vertical) } else { - Button(L10n.signIn) { + ListRowButton(L10n.signIn) { viewModel.send(.signIn(username: username, password: password, policy: .none)) } .disabled(username.isEmpty) .foregroundStyle( accentColor.overlayColor, - accentColor + username.isEmpty ? Color.white.opacity(0.5) : accentColor ) .opacity(username.isEmpty ? 0.5 : 1) + .padding(.vertical) } if viewModel.isQuickConnectEnabled { @@ -114,14 +113,17 @@ struct UserSignInView: View { accentColor.overlayColor, accentColor ) + .padding(.bottom) } } if let disclaimer = viewModel.serverDisclaimer { Section(L10n.disclaimer) { Text(disclaimer) + .foregroundStyle(.secondary) .font(.callout) } + .padding(.top) } } @@ -129,22 +131,30 @@ struct UserSignInView: View { @ViewBuilder private var publicUsersSection: some View { - Section(L10n.publicUsers) { - if viewModel.publicUsers.isEmpty { - L10n.noPublicUsers.text - .font(.callout) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity) - } else { + if viewModel.publicUsers.isEmpty { + L10n.noPublicUsers.text + .font(.callout) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .center) + .frame(maxHeight: .infinity, alignment: .center) + } else { + LazyVGrid( + columns: Array(repeating: GridItem(.flexible()), count: 4), + spacing: 30 + ) { ForEach(viewModel.publicUsers, id: \.id) { user in - PublicUserRow( + PublicUserButton( user: user, client: viewModel.server.client ) { username = user.name ?? "" password = "" - focusedTextField = .password + focusedField = .password } + .environment( + \.isEnabled, + viewModel.state != .signingIn + ) } } } @@ -153,34 +163,14 @@ struct UserSignInView: View { // MARK: - Body var body: some View { - VStack { - HStack { - Spacer() - - if viewModel.state == .signingIn { - ProgressView() - } - } - .frame(height: 100) - .overlay { - Image(.jellyfinBlobBlue) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 100) - .edgePadding() - } - - HStack(alignment: .top) { - VStack(alignment: .leading) { - signInSection - } - - VStack(alignment: .leading) { - publicUsersSection - } - } - - Spacer() + SplitLoginWindowView( + isLoading: viewModel.state == .signingIn, + leadingTitle: L10n.signInToServer(viewModel.server.name), + trailingTitle: L10n.publicUsers + ) { + signInSection + } trailingContentView: { + publicUsersSection } .onReceive(viewModel.events) { event in switch event { @@ -198,7 +188,7 @@ struct UserSignInView: View { } } .onFirstAppear { - focusedTextField = .username + focusedField = .username viewModel.send(.getPublicData) } .alert( @@ -209,11 +199,11 @@ struct UserSignInView: View { // TODO: uncomment when duplicate user fixed // Button(L10n.signIn) { -// signInUplicate(user: user, replace: false) +// signInDuplicate(user: user, replace: false) // } // Button("Replace") { -// signInUplicate(user: user, replace: true) +// signInDuplicate(user: user, replace: true) // } Button(L10n.dismiss, role: .cancel) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index f151b373..6e158206 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -84,6 +84,8 @@ 4E49DEE42CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */; }; 4E49DEE62CE5616800352DCD /* UserProfileImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */; }; 4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; }; + 4E4DAC372D11EE5E00E13FF9 /* SplitLoginWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */; }; + 4E4DAC3D2D11F94400E13FF9 /* LocalServerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */; }; 4E4E9C672CFEBF2A00A6946F /* StudioEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */; }; 4E4E9C682CFEBF2A00A6946F /* StudioEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */; }; 4E4E9C6A2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C692CFEDC9D00A6946F /* PeopleEditorViewModel.swift */; }; @@ -158,6 +160,8 @@ 4E90F76A2CC72B1F00417C31 /* DetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F7592CC72B1F00417C31 /* DetailsSection.swift */; }; 4E97D1832D064748004B89AD /* ItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97D1822D064748004B89AD /* ItemSection.swift */; }; 4E97D1852D064B43004B89AD /* RefreshMetadataButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */; }; + 4E98F7D22D123AD4001E7518 /* NavigationBarMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98F7C12D123AD4001E7518 /* NavigationBarMenuButton.swift */; }; + 4E98F7D32D123AD4001E7518 /* View-tvOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98F7C92D123AD4001E7518 /* View-tvOS.swift */; }; 4E9A24E62C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24E52C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift */; }; 4E9A24E82C82B6190023DA83 /* CustomProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */; }; 4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; }; @@ -759,7 +763,7 @@ E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */; }; E1763A642BF3C9AA004DF6AB /* ListRowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */; }; E1763A662BF3CA83004DF6AB /* FullScreenMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */; }; - E1763A6A2BF3D177004DF6AB /* PublicUserRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A692BF3D177004DF6AB /* PublicUserRow.swift */; }; + E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A692BF3D177004DF6AB /* PublicUserButton.swift */; }; E1763A712BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; }; E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; }; E1763A742BF3FA4C004DF6AB /* AppLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A732BF3FA4C004DF6AB /* AppLoadingView.swift */; }; @@ -1227,6 +1231,8 @@ 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareImageCropView.swift; sourceTree = ""; }; 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPlayUserAccessType.swift; sourceTree = ""; }; 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImagePicker.swift; sourceTree = ""; }; + 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitLoginWindowView.swift; sourceTree = ""; }; + 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalServerButton.swift; sourceTree = ""; }; 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudioEditorViewModel.swift; sourceTree = ""; }; 4E4E9C692CFEDC9D00A6946F /* PeopleEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeopleEditorViewModel.swift; sourceTree = ""; }; 4E5071D62CFCEB6F003FA2AD /* TagEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagEditorViewModel.swift; sourceTree = ""; }; @@ -1283,6 +1289,8 @@ 4E90F7612CC72B1F00417C31 /* EditServerTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServerTaskView.swift; sourceTree = ""; }; 4E97D1822D064748004B89AD /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = ""; }; 4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshMetadataButton.swift; sourceTree = ""; }; + 4E98F7C12D123AD4001E7518 /* NavigationBarMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarMenuButton.swift; sourceTree = ""; }; + 4E98F7C92D123AD4001E7518 /* View-tvOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View-tvOS.swift"; sourceTree = ""; }; 4E9A24E52C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileSettingsView.swift; sourceTree = ""; }; 4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProfileButton.swift; sourceTree = ""; }; 4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileCoordinator.swift; sourceTree = ""; }; @@ -1689,7 +1697,7 @@ E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGridButton.swift; sourceTree = ""; }; E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowButton.swift; sourceTree = ""; }; E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMenu.swift; sourceTree = ""; }; - E1763A692BF3D177004DF6AB /* PublicUserRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserRow.swift; sourceTree = ""; }; + E1763A692BF3D177004DF6AB /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = ""; }; E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+Mappings.swift"; sourceTree = ""; }; E1763A732BF3FA4C004DF6AB /* AppLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLoadingView.swift; sourceTree = ""; }; E1763A752BF3FF01004DF6AB /* AppLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLoadingView.swift; sourceTree = ""; }; @@ -2260,6 +2268,23 @@ path = UserProfileImagePicker; sourceTree = ""; }; + 4E4DAC3A2D11F54300E13FF9 /* ConnectToServerView */ = { + isa = PBXGroup; + children = ( + 4E4DAC3B2D11F69000E13FF9 /* Components */, + 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, + ); + path = ConnectToServerView; + sourceTree = ""; + }; + 4E4DAC3B2D11F69000E13FF9 /* Components */ = { + isa = PBXGroup; + children = ( + 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */, + ); + path = Components; + sourceTree = ""; + }; 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */ = { isa = PBXGroup; children = ( @@ -2499,6 +2524,31 @@ path = EditServerTaskView; sourceTree = ""; }; + 4E98F7C82D123AD4001E7518 /* Modifiers */ = { + isa = PBXGroup; + children = ( + 4E98F7C12D123AD4001E7518 /* NavigationBarMenuButton.swift */, + ); + path = Modifiers; + sourceTree = ""; + }; + 4E98F7CA2D123AD4001E7518 /* View */ = { + isa = PBXGroup; + children = ( + 4E98F7C82D123AD4001E7518 /* Modifiers */, + 4E98F7C92D123AD4001E7518 /* View-tvOS.swift */, + ); + path = View; + sourceTree = ""; + }; + 4E98F7CB2D123AD4001E7518 /* Extensions */ = { + isa = PBXGroup; + children = ( + 4E98F7CA2D123AD4001E7518 /* View */, + ); + path = Extensions; + sourceTree = ""; + }; 4E9A24E32C82B4700023DA83 /* CustomDeviceProfileSettingsView */ = { isa = PBXGroup; children = ( @@ -2839,6 +2889,7 @@ children = ( E12186DF2718F2030010884C /* App */, 536D3D77267BB9650004248C /* Components */, + 4E98F7CB2D123AD4001E7518 /* Extensions */, E185920B28CEF23F00326F80 /* Objects */, E1DABAD62A26E28E008AC34A /* Resources */, E12186E02718F23B0010884C /* Views */, @@ -2941,6 +2992,7 @@ E1E9EFE928C6B96400CC1F8B /* ServerButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */, E12E30F0296383810022FAC9 /* SplitFormWindowView.swift */, + 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */, E187A60429AD2E25008387E6 /* StepperView.swift */, ); path = Components; @@ -3673,7 +3725,7 @@ E1763A752BF3FF01004DF6AB /* AppLoadingView.swift */, E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */, E10231522BCF8AF8009D71FC /* ChannelLibraryView */, - 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, + 4E4DAC3A2D11F54300E13FF9 /* ConnectToServerView */, E154967B296CBB1A00C4EF88 /* FontPickerView.swift */, E1A42E4D28CBD3B200A14DCB /* HomeView */, E12376B22A33DFAC001F5B44 /* ItemOverviewView.swift */, @@ -4001,7 +4053,7 @@ E1763A682BF3D16E004DF6AB /* Components */ = { isa = PBXGroup; children = ( - E1763A692BF3D177004DF6AB /* PublicUserRow.swift */, + E1763A692BF3D177004DF6AB /* PublicUserButton.swift */, ); path = Components; sourceTree = ""; @@ -5043,6 +5095,8 @@ E1575E99293E7B1E001665B1 /* UIColor.swift in Sources */, E1575E92293E7B1E001665B1 /* CGSize.swift in Sources */, E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */, + 4E98F7D22D123AD4001E7518 /* NavigationBarMenuButton.swift in Sources */, + 4E98F7D32D123AD4001E7518 /* View-tvOS.swift in Sources */, C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */, C46DD8EA2A8FB45C0046A504 /* LiveOverlay.swift in Sources */, E11E376D293E9CC1009EF240 /* VideoPlayerCoordinator.swift in Sources */, @@ -5056,6 +5110,7 @@ 4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */, 4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, + 4E4DAC372D11EE5E00E13FF9 /* SplitLoginWindowView.swift in Sources */, 4E97D1832D064748004B89AD /* ItemSection.swift in Sources */, E145EB232BDCCA43003BF6F3 /* BulletedList.swift in Sources */, E104DC972B9E7E29008F506D /* AssertionFailureView.swift in Sources */, @@ -5263,6 +5318,7 @@ E1B4E4372CA7795200DC49DE /* OrderedDictionary.swift in Sources */, E1AD104E26D96CE3003E4A08 /* BaseItemDto.swift in Sources */, E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */, + 4E4DAC3D2D11F94400E13FF9 /* LocalServerButton.swift in Sources */, 62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */, BD0BA22C2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */, E1575EA2293E7B1E001665B1 /* Color.swift in Sources */, @@ -5313,7 +5369,7 @@ E1575E7D293E77B5001665B1 /* PosterDisplayType.swift in Sources */, E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */, E18A17F2298C68BB00C22F62 /* MainOverlay.swift in Sources */, - E1763A6A2BF3D177004DF6AB /* PublicUserRow.swift in Sources */, + E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */, E1E6C44B29AED2B70064123F /* HorizontalAlignment.swift in Sources */, 4E35CE672CBED8B600DBD886 /* ServerTicks.swift in Sources */, E193D549271941CC00900D82 /* UserSignInView.swift in Sources */,