[tvOS] Move AddUserButton to the bottom bar of Select User View (#1468)

* Added add user button to select user bottom bar

* Replaced AddUserButton with NoUserView

This commit removes the AddUserButton as it is no longer required.
Also when no user is logged in the GridView shows a new NoUserView

* Added multi server support

- When no user is logged in. Grid view shows the original AddUserButton.
- When there is a logged in user AddUserButton is replaced with AddUserBottomButton
- AddUserBottomButton will show a menu when in all server mode (just like AddUserbutton)
- Removed NoUserView as it isn't required anymore
- changed bottom bar layout to  allow for a larger service selection button

* cleaned up AddUserBottomButton

* cleaned up AddUserBottomButton

fixed formatting

* cleanup

* fix conflict

* cleaned up unused localisation

* cleanup

* removed debug background

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Glenn Hevey 2025-04-07 06:51:44 +10:00 committed by GitHub
parent 216375905c
commit e6cc848138
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 507 additions and 368 deletions

View File

@ -28,8 +28,9 @@ struct AlternateLayoutView<Content: View, Layout: View>: View {
var body: some View { var body: some View {
layout() layout()
.hidden() .hidden()
.overlay(alignment: alignment) { .overlay(
content() alignment: alignment,
} content: content
)
} }
} }

View File

@ -0,0 +1,86 @@
//
// 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) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
/// A LazyVGrid that centers its elements, most notably on the last row.
struct CenteredLazyVGrid<Data: RandomAccessCollection, ID: Hashable, Content: View>: View {
@State
private var elementSize: CGSize = .zero
private let columnCount: Int
private let columns: [GridItem]
private let content: (Data.Element) -> Content
private let data: Data
private let id: KeyPath<Data.Element, ID>
private let spacing: CGFloat
/// Calculates the x offset for elements in
/// the last row of the grid to be centered.
private func elementXOffset(for offset: Int) -> CGFloat {
let dataCount = data.count
let lastRowCount = dataCount % columnCount
guard lastRowCount > 0 else { return 0 }
let lastRowIndices = (dataCount - lastRowCount ..< dataCount)
guard lastRowIndices.contains(offset) else { return 0 }
let lastRowMissingCount = columnCount - lastRowCount
return CGFloat(lastRowMissingCount) * (elementSize.width + spacing) / 2
}
var body: some View {
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(Array(data.enumerated()), id: \.offset) { offset, element in
content(element)
.trackingSize($elementSize)
.offset(x: elementXOffset(for: offset))
}
}
}
}
extension CenteredLazyVGrid {
init(
data: Data,
id: KeyPath<Data.Element, ID>,
columns: Int,
spacing: CGFloat = 0,
@ViewBuilder content: @escaping (Data.Element) -> Content
) {
self.columnCount = columns
self.content = content
self.data = data
self.id = id
self.spacing = spacing
self.columns = Array(repeating: GridItem(.flexible(), spacing: spacing), count: columns)
}
}
extension CenteredLazyVGrid where Data.Element: Identifiable, ID == Data.Element.ID {
init(
data: Data,
columns: Int,
spacing: CGFloat = 0,
@ViewBuilder content: @escaping (Data.Element) -> Content
) {
self.init(
data: data,
id: \.id,
columns: columns,
spacing: spacing,
content: content
)
}
}

View File

@ -0,0 +1,63 @@
//
// 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) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: Figure out workaround with extra padding from `Menu`
struct ConditionalMenu<Label: View, MenuContent: View>: View {
private let action: () -> Void
private let isMenu: Bool
private let label: () -> Label
private let menuContent: () -> MenuContent
var body: some View {
if isMenu {
Menu(
content: menuContent,
label: label
)
} else {
Button(
action: action,
label: label
)
}
}
}
extension ConditionalMenu {
init<V>(
tracking data: V?,
action: @escaping (V) -> Void,
@ViewBuilder menuContent: @escaping () -> MenuContent,
@ViewBuilder label: @escaping () -> Label
) where V: Identifiable {
self.action = {
guard let data else { return }
action(data)
}
self.isMenu = data == nil
self.label = label
self.menuContent = menuContent
}
init(
isMenu: Bool,
action: @escaping () -> Void,
@ViewBuilder menuContent: @escaping () -> MenuContent,
@ViewBuilder label: @escaping () -> Label
) {
self.action = action
self.isMenu = isMenu
self.label = label
self.menuContent = menuContent
}
}

View File

@ -9,6 +9,7 @@
import SwiftUI import SwiftUI
// TODO: both axes // TODO: both axes
// TODO: add scrollClipDisabled() to iOS when iOS 15 dropped
struct ScrollIfLargerThanContainerModifier: ViewModifier { struct ScrollIfLargerThanContainerModifier: ViewModifier {
@ -28,6 +29,9 @@ struct ScrollIfLargerThanContainerModifier: ViewModifier {
content content
.trackingSize($contentSize) .trackingSize($contentSize)
} }
#if os(tvOS)
.scrollClipDisabled()
#endif
.frame(maxHeight: contentSize.height >= layoutSize.height ? .infinity : contentSize.height) .frame(maxHeight: contentSize.height >= layoutSize.height ? .infinity : contentSize.height)
.backport .backport
.scrollDisabled(contentSize.height < layoutSize.height) .scrollDisabled(contentSize.height < layoutSize.height)

View File

@ -251,6 +251,16 @@ extension View {
opacity(isVisible ? 1 : 0) opacity(isVisible ? 1 : 0)
} }
@inlinable
@ViewBuilder
func hidden(_ isHidden: Bool) -> some View {
if isHidden {
hidden()
} else {
self
}
}
func blurred(style: UIBlurEffect.Style = .regular) -> some View { func blurred(style: UIBlurEffect.Style = .regular) -> some View {
overlay { overlay {
BlurView(style: style) BlurView(style: style)

View File

@ -7,9 +7,8 @@
// //
import Defaults import Defaults
import Foundation
enum SelectUserServerSelection: RawRepresentable, Codable, Defaults.Serializable, Equatable, Hashable { enum SelectUserServerSelection: RawRepresentable, Hashable, Storable {
case all case all
case server(id: String) case server(id: String)
@ -31,4 +30,13 @@ enum SelectUserServerSelection: RawRepresentable, Codable, Defaults.Serializable
self = .server(id: rawValue) self = .server(id: rawValue)
} }
} }
func server<S: Sequence>(from servers: S) -> ServerState? where S.Element == ServerState {
switch self {
case .all:
return nil
case let .server(id):
return servers.first { $0.id == id }
}
}
} }

View File

@ -0,0 +1,67 @@
//
// 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) 2025 Jellyfin & Jellyfin Contributors
//
import OrderedCollections
import SwiftUI
extension SelectUserView {
struct AddUserBottomButton: View {
// MARK: Properties
private let action: (ServerState) -> Void
private let selectedServer: ServerState?
private let servers: OrderedSet<ServerState>
// MARK: View Builders
@ViewBuilder
private var label: some View {
Label(L10n.addUser, systemImage: "plus")
.foregroundStyle(Color.primary)
.font(.body.weight(.semibold))
.labelStyle(.iconOnly)
.frame(width: 50, height: 50)
}
// MARK: - Initializer
init(
selectedServer: ServerState?,
servers: OrderedSet<ServerState>,
action: @escaping (ServerState) -> Void
) {
self.action = action
self.selectedServer = selectedServer
self.servers = servers
}
// MARK: Body
var body: some View {
ConditionalMenu(
tracking: selectedServer,
action: action
) {
Text(L10n.selectServer)
ForEach(servers) { server in
Button {
action(server)
} label: {
Text(server.name)
Text(server.currentURL.absoluteString)
}
}
} label: {
label
}
}
}
}

View File

@ -1,101 +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) 2025 Jellyfin & Jellyfin Contributors
//
import OrderedCollections
import SwiftUI
extension SelectUserView {
struct AddUserButton: View {
@Binding
private var serverSelection: SelectUserServerSelection
@Environment(\.isEnabled)
private var isEnabled
private let action: (ServerState) -> Void
private let servers: OrderedSet<ServerState>
private var selectedServer: ServerState? {
if case let SelectUserServerSelection.server(id: id) = serverSelection,
let server = servers.first(where: { server in server.id == id })
{
return server
}
return nil
}
init(
serverSelection: Binding<SelectUserServerSelection>,
servers: OrderedSet<ServerState>,
action: @escaping (ServerState) -> Void
) {
self._serverSelection = serverSelection
self.action = action
self.servers = servers
}
@ViewBuilder
private var content: some View {
ZStack {
Color.secondarySystemFill
RelativeSystemImageView(systemName: "plus")
.foregroundStyle(.secondary)
}
.clipShape(.circle)
.aspectRatio(1, contentMode: .fill)
.hoverEffect(.highlight)
Text(L10n.addUser)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(isEnabled ? .primary : .secondary)
if serverSelection == .all {
// For layout, not to be localized
Text("Hidden")
.font(.footnote)
.hidden()
}
}
var body: some View {
if serverSelection == .all {
Menu {
Text(L10n.selectServer)
ForEach(servers) { server in
Button {
action(server)
} label: {
Text(server.name)
Text(server.currentURL.absoluteString)
}
}
} label: {
content
}
.buttonStyle(.borderless)
.buttonBorderShape(.circle)
} else {
Button {
if let selectedServer {
action(selectedServer)
}
} label: {
content
}
.buttonStyle(.borderless)
.buttonBorderShape(.circle)
}
}
}
}

View File

@ -0,0 +1,70 @@
//
// 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) 2025 Jellyfin & Jellyfin Contributors
//
import OrderedCollections
import SwiftUI
extension SelectUserView {
struct AddUserGridButton: View {
@Environment(\.isEnabled)
private var isEnabled
let selectedServer: ServerState?
let servers: OrderedSet<ServerState>
let action: (ServerState) -> Void
@ViewBuilder
private var label: some View {
ZStack {
Color.secondarySystemFill
RelativeSystemImageView(systemName: "plus")
.foregroundStyle(.secondary)
}
.clipShape(.circle)
.aspectRatio(1, contentMode: .fill)
.hoverEffect(.highlight)
Text(L10n.addUser)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(isEnabled ? .primary : .secondary)
if selectedServer == nil {
// For layout, not to be localized
Text("Hidden")
.font(.footnote)
.hidden()
}
}
var body: some View {
ConditionalMenu(
tracking: selectedServer,
action: action
) {
Text(L10n.selectServer)
ForEach(servers) { server in
Button {
action(server)
} label: {
Text(server.name)
Text(server.currentURL.absoluteString)
}
}
} label: {
label
}
.buttonStyle(.borderless)
.buttonBorderShape(.circle)
}
}
}

View File

@ -6,6 +6,7 @@
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // Copyright (c) 2025 Jellyfin & Jellyfin Contributors
// //
import OrderedCollections
import SwiftUI import SwiftUI
extension SelectUserView { extension SelectUserView {
@ -23,17 +24,38 @@ extension SelectUserView {
@Binding @Binding
private var serverSelection: SelectUserServerSelection private var serverSelection: SelectUserServerSelection
@ObservedObject
private var viewModel: SelectUserViewModel
// MARK: - Variables // MARK: - Variables
private let areUsersSelected: Bool private let areUsersSelected: Bool
private let userCount: Int private let hasUsers: Bool
private let selectedServer: ServerState?
private let servers: OrderedSet<ServerState>
private let onDelete: () -> Void private let onDelete: () -> Void
private let toggleAllUsersSelected: () -> Void private let toggleAllUsersSelected: () -> Void
// MARK: - Initializer
init(
isEditing: Binding<Bool>,
serverSelection: Binding<SelectUserServerSelection>,
selectedServer: ServerState?,
servers: OrderedSet<ServerState>,
areUsersSelected: Bool,
hasUsers: Bool,
onDelete: @escaping () -> Void,
toggleAllUsersSelected: @escaping () -> Void
) {
self._isEditing = isEditing
self._serverSelection = serverSelection
self.areUsersSelected = areUsersSelected
self.hasUsers = hasUsers
self.selectedServer = selectedServer
self.servers = servers
self.onDelete = onDelete
self.toggleAllUsersSelected = toggleAllUsersSelected
}
// MARK: - Advanced Menu // MARK: - Advanced Menu
@ViewBuilder @ViewBuilder
@ -74,39 +96,22 @@ extension SelectUserView {
// MARK: - Delete User Button // MARK: - Delete User Button
@ViewBuilder
private var deleteUsersButton: some View { private var deleteUsersButton: some View {
ListRowButton(L10n.delete, role: .destructive) { ListRowButton(
onDelete() L10n.delete,
} role: .destructive,
action: onDelete
)
.frame(width: 400, height: 75) .frame(width: 400, height: 75)
.disabled(!areUsersSelected) .disabled(!areUsersSelected)
} }
// MARK: - Initializer
init(
isEditing: Binding<Bool>,
serverSelection: Binding<SelectUserServerSelection>,
areUsersSelected: Bool,
viewModel: SelectUserViewModel,
userCount: Int,
onDelete: @escaping () -> Void,
toggleAllUsersSelected: @escaping () -> Void
) {
self._isEditing = isEditing
self._serverSelection = serverSelection
self.viewModel = viewModel
self.areUsersSelected = areUsersSelected
self.userCount = userCount
self.onDelete = onDelete
self.toggleAllUsersSelected = toggleAllUsersSelected
}
// MARK: - Content View // MARK: - Content View
@ViewBuilder @ViewBuilder
private var contentView: some View { private var contentView: some View {
HStack(alignment: .center) { HStack(alignment: .top, spacing: 20) {
if isEditing { if isEditing {
deleteUsersButton deleteUsersButton
@ -130,9 +135,18 @@ extension SelectUserView {
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 10))
} }
} else { } else {
AddUserBottomButton(
selectedServer: selectedServer,
servers: servers
) { server in
router.route(to: \.userSignIn, server)
}
.hidden(!hasUsers)
ServerSelectionMenu( ServerSelectionMenu(
selection: $serverSelection, selection: $serverSelection,
viewModel: viewModel selectedServer: selectedServer,
servers: servers
) )
advancedMenu advancedMenu

View File

@ -6,6 +6,7 @@
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // Copyright (c) 2025 Jellyfin & Jellyfin Contributors
// //
import OrderedCollections
import SwiftUI import SwiftUI
extension SelectUserView { extension SelectUserView {
@ -17,32 +18,46 @@ extension SelectUserView {
@EnvironmentObject @EnvironmentObject
private var router: SelectUserCoordinator.Router private var router: SelectUserCoordinator.Router
@ObservedObject
private var viewModel: SelectUserViewModel
// MARK: - Server Selection // MARK: - Server Selection
@Binding @Binding
private var serverSelection: SelectUserServerSelection private var serverSelection: SelectUserServerSelection
private var selectedServer: ServerState? { private let selectedServer: ServerState?
if case let SelectUserServerSelection.server(id: id) = serverSelection, private let servers: OrderedSet<ServerState>
let server = viewModel.servers.keys.first(where: { server in server.id == id })
{
return server
}
return nil
}
// MARK: - Initializer // MARK: - Initializer
init( init(
selection: Binding<SelectUserServerSelection>, selection: Binding<SelectUserServerSelection>,
viewModel: SelectUserViewModel selectedServer: ServerState?,
servers: OrderedSet<ServerState>
) { ) {
self._serverSelection = selection self._serverSelection = selection
self.viewModel = viewModel self.selectedServer = selectedServer
self.servers = servers
}
@ViewBuilder
private var label: some View {
HStack(spacing: 16) {
if let selectedServer {
Image(systemName: "server.rack")
Text(selectedServer.name)
} else {
Image(systemName: "person.2.fill")
Text(L10n.allServers)
}
Image(systemName: "chevron.up.chevron.down")
.foregroundStyle(.secondary)
.font(.subheadline.weight(.semibold))
}
.font(.body.weight(.semibold))
.foregroundStyle(Color.primary)
.frame(width: 400, height: 50)
} }
// MARK: - Body // MARK: - Body
@ -50,14 +65,15 @@ extension SelectUserView {
var body: some View { var body: some View {
Menu { Menu {
Picker(L10n.servers, selection: _serverSelection) { Picker(L10n.servers, selection: _serverSelection) {
ForEach(viewModel.servers.keys) { server in ForEach(servers) { server in
Button { Button {
Text(server.name) Text(server.name)
Text(server.currentURL.absoluteString) Text(server.currentURL.absoluteString)
} }
.tag(SelectUserServerSelection.server(id: server.id)) .tag(SelectUserServerSelection.server(id: server.id))
} }
if viewModel.servers.keys.count > 1 {
if servers.count > 1 {
Label(L10n.allServers, systemImage: "person.2.fill") Label(L10n.allServers, systemImage: "person.2.fill")
.tag(SelectUserServerSelection.all) .tag(SelectUserServerSelection.all)
} }
@ -68,29 +84,13 @@ extension SelectUserView {
router.route(to: \.editServer, selectedServer) router.route(to: \.editServer, selectedServer)
} }
} }
Button(L10n.addServer, systemImage: "plus") { Button(L10n.addServer, systemImage: "plus") {
router.route(to: \.connectToServer) router.route(to: \.connectToServer)
} }
} }
} label: { } label: {
HStack(spacing: 16) { label
switch serverSelection {
case .all:
Image(systemName: "person.2.fill")
Text(L10n.allServers)
case let .server(id):
if let server = viewModel.servers.keys.first(where: { $0.id == id }) {
Image(systemName: "server.rack")
Text(server.name)
}
}
Image(systemName: "chevron.up.chevron.down")
.foregroundStyle(.secondary)
.font(.subheadline.weight(.semibold))
}
.font(.body.weight(.semibold))
.foregroundStyle(Color.primary)
.frame(width: 400, height: 50)
} }
.menuOrder(.fixed) .menuOrder(.fixed)
} }

View File

@ -19,70 +19,38 @@ struct SelectUserView: View {
// MARK: - User Grid Item Enum // MARK: - User Grid Item Enum
private enum UserGridItem: Hashable { typealias UserGridItem = (user: UserState, server: ServerState)
case user(UserState, server: ServerState)
case addUser
}
// MARK: - Defaults // MARK: - Defaults
@Default(.selectUserServerSelection)
private var serverSelection
@Default(.selectUserUseSplashscreen) @Default(.selectUserUseSplashscreen)
private var selectUserUseSplashscreen private var selectUserUseSplashscreen
@Default(.selectUserAllServersSplashscreen)
// MARK: - Environment Variable private var selectUserAllServersSplashscreen
@Default(.selectUserServerSelection)
@Environment(\.colorScheme) private var serverSelection
private var colorScheme
// MARK: - State & Environment Objects // MARK: - State & Environment Objects
@EnvironmentObject @EnvironmentObject
private var router: SelectUserCoordinator.Router private var router: SelectUserCoordinator.Router
@StateObject
private var viewModel = SelectUserViewModel()
// MARK: - Select User Variables // MARK: - Select User Variables
@State
private var contentSize: CGSize = .zero
@State
private var gridItems: OrderedSet<UserGridItem> = []
@State
private var gridItemSize: CGSize = .zero
@State @State
private var isEditingUsers: Bool = false private var isEditingUsers: Bool = false
@State @State
private var padGridItemColumnCount: Int = 1
@State
private var pin: String = "" private var pin: String = ""
@State @State
private var scrollViewOffset: CGFloat = 0 private var scrollViewOffset: CGFloat = 0
@State @State
private var selectedUsers: Set<UserState> = [] private var selectedUsers: Set<UserState> = []
@State
private var splashScreenImageSource: ImageSource? = nil
private var users: [UserState] {
gridItems.compactMap { item in
switch item {
case let .user(user, _):
return user
default:
return nil
}
}
}
// MARK: - Dialog States // MARK: - Dialog States
@State @State
private var isPresentingConfirmDeleteUsers = false private var isPresentingConfirmDeleteUsers = false
@State @State
private var isPresentingServers: Bool = false
@State
private var isPresentingLocalPin: Bool = false private var isPresentingLocalPin: Bool = false
// MARK: - Error State // MARK: - Error State
@ -90,136 +58,89 @@ struct SelectUserView: View {
@State @State
private var error: Error? = nil private var error: Error? = nil
// MARK: - Selected Server @StateObject
private var viewModel = SelectUserViewModel()
private var selectedServer: ServerState? { private var selectedServer: ServerState? {
if case let SelectUserServerSelection.server(id: id) = serverSelection, serverSelection.server(from: viewModel.servers.keys)
let server = viewModel.servers.keys.first(where: { server in server.id == id })
{
return server
}
return nil
} }
// MARK: - Make Grid Items private var splashScreenImageSources: [ImageSource] {
switch (serverSelection, selectUserAllServersSplashscreen) {
case (.all, .all):
return viewModel
.servers
.keys
.shuffled()
.map { $0.splashScreenImageSource() }
private func makeGridItems(for serverSelection: SelectUserServerSelection) -> OrderedSet<UserGridItem> { // need to evaluate server with id selection first
case let (.server(id), _), let (.all, .server(id)):
guard let imageSource = viewModel
.servers
.keys
.first(where: { $0.id == id }) else { return [] }
return [imageSource.splashScreenImageSource()]
}
}
private var userGridItems: [UserGridItem] {
switch serverSelection { switch serverSelection {
case .all: case .all:
let items = viewModel.servers return viewModel.servers
.map { server, users in .map { server, users in
users.map { (server: server, user: $0) } users.map { (server: server, user: $0) }
} }
.flatMap { $0 } .flatMap { $0 }
.sorted(using: \.user.username) .sorted(using: \.user.username)
.reversed() .reversed()
.map { UserGridItem.user($0.user, server: $0.server) } .map { UserGridItem(user: $0.user, server: $0.server) }
.appending(.addUser)
return OrderedSet(items)
case let .server(id: id): case let .server(id: id):
guard let server = viewModel.servers.keys.first(where: { server in server.id == id }) else { guard let server = viewModel.servers.keys.first(where: { server in server.id == id }) else {
assertionFailure("server with ID not found?") assertionFailure("server with ID not found?")
return [.addUser] return []
} }
let items = viewModel.servers[server]! return viewModel.servers[server]!
.sorted(using: \.username) .sorted(using: \.username)
.map { UserGridItem.user($0, server: server) } .map { UserGridItem(user: $0, server: server) }
.appending(.addUser)
return OrderedSet(items)
} }
} }
// MARK: - Make Splash Screen Image Source
// For all server selection, .all is random
private func makeSplashScreenImageSource(
serverSelection: SelectUserServerSelection,
allServersSelection: SelectUserServerSelection
) -> ImageSource? {
switch (serverSelection, allServersSelection) {
case (.all, .all):
return viewModel
.servers
.keys
.randomElement()?
.splashScreenImageSource()
// need to evaluate server with id selection first
case let (.server(id), _), let (.all, .server(id)):
return viewModel
.servers
.keys
.first { $0.id == id }?
.splashScreenImageSource()
}
}
// MARK: - Grid Item Offset
private func gridItemOffset(index: Int) -> CGFloat {
let lastRowIndices = (gridItems.count - gridItems.count % padGridItemColumnCount ..< gridItems.count)
guard lastRowIndices.contains(index) else { return 0 }
let lastRowMissing = padGridItemColumnCount - gridItems.count % padGridItemColumnCount
return CGFloat(lastRowMissing) * (gridItemSize.width + EdgeInsets.edgePadding) / 2
}
// MARK: - Select User(s) // MARK: - Select User(s)
private func select(user: UserState, needsPin: Bool = true) { private func select(user: UserState, needsPin: Bool = true) {
Task { @MainActor in selectedUsers.insert(user)
selectedUsers.insert(user)
switch user.accessPolicy { switch user.accessPolicy {
case .requireDeviceAuthentication: case .requireDeviceAuthentication:
// Do nothing, no device authentication on tvOS // Do nothing, no device authentication on tvOS
break break
case .requirePin: case .requirePin:
if needsPin { if needsPin {
isPresentingLocalPin = true isPresentingLocalPin = true
return return
}
case .none: ()
} }
case .none: ()
viewModel.send(.signIn(user, pin: pin))
} }
viewModel.send(.signIn(user, pin: pin))
} }
// MARK: - Grid Content View // MARK: - Grid Content View
@ViewBuilder @ViewBuilder
private var gridContentView: some View { private var userGrid: some View {
let columns = Array(repeating: GridItem(.flexible(), spacing: EdgeInsets.edgePadding), count: 5) CenteredLazyVGrid(
data: userGridItems,
id: \.user.id,
columns: 5,
spacing: EdgeInsets.edgePadding
) { gridItem in
let user = gridItem.user
let server = gridItem.server
LazyVGrid(columns: columns, spacing: EdgeInsets.edgePadding) {
ForEach(Array(gridItems.enumerated().map(\.offset)), id: \.hashValue) { index in
let item = gridItems[index]
gridItemView(for: item)
.trackingSize($gridItemSize)
.offset(x: gridItemOffset(index: index))
}
}
.padding(EdgeInsets.edgePadding * 2.5)
.onChange(of: gridItemSize) { _, newValue in
let columns = Int(contentSize.width / (newValue.width + EdgeInsets.edgePadding))
padGridItemColumnCount = columns
}
}
// MARK: - Grid Content View
@ViewBuilder
private func gridItemView(for item: UserGridItem) -> some View {
switch item {
case let .user(user, server):
UserGridButton( UserGridButton(
user: user, user: user,
server: server, server: server,
@ -234,35 +155,46 @@ struct SelectUserView: View {
selectedUsers.insert(user) selectedUsers.insert(user)
isPresentingConfirmDeleteUsers = true isPresentingConfirmDeleteUsers = true
} }
.environment(\.isEditing, isEditingUsers) }
.environment(\.isSelected, selectedUsers.contains(user)) }
case .addUser:
AddUserButton( @ViewBuilder
serverSelection: $serverSelection, private var addUserButtonGrid: some View {
CenteredLazyVGrid(
data: [0],
id: \.self,
columns: 5
) { _ in
AddUserGridButton(
selectedServer: selectedServer,
servers: viewModel.servers.keys servers: viewModel.servers.keys
) { server in ) { server in
router.route(to: \.userSignIn, server) router.route(to: \.userSignIn, server)
} }
.environment(\.isEnabled, !isEditingUsers)
} }
} }
// MARK: - User View // MARK: - User View
@ViewBuilder @ViewBuilder
private var userView: some View { private var contentView: some View {
VStack { VStack {
ZStack { ZStack {
Color.clear Color.clear
.trackingSize($contentSize)
VStack(spacing: 0) { VStack(spacing: 0) {
Color.clear Color.clear
.frame(height: 100) .frame(height: 100)
gridContentView Group {
.focusSection() if userGridItems.isEmpty {
addUserButtonGrid
} else {
userGrid
}
}
.focusSection()
} }
.scrollIfLargerThanContainer(padding: 100) .scrollIfLargerThanContainer(padding: 100)
.scrollViewOffset($scrollViewOffset) .scrollViewOffset($scrollViewOffset)
@ -271,30 +203,31 @@ struct SelectUserView: View {
SelectUserBottomBar( SelectUserBottomBar(
isEditing: $isEditingUsers, isEditing: $isEditingUsers,
serverSelection: $serverSelection, serverSelection: $serverSelection,
selectedServer: selectedServer,
servers: viewModel.servers.keys,
areUsersSelected: selectedUsers.isNotEmpty, areUsersSelected: selectedUsers.isNotEmpty,
viewModel: viewModel, hasUsers: userGridItems.isNotEmpty
userCount: gridItems.count,
onDelete: {
isPresentingConfirmDeleteUsers = true
}
) { ) {
if selectedUsers.count == users.count { isPresentingConfirmDeleteUsers = true
} toggleAllUsersSelected: {
if selectedUsers.isNotEmpty {
selectedUsers.removeAll() selectedUsers.removeAll()
} else { } else {
selectedUsers.insert(contentsOf: users) selectedUsers.insert(contentsOf: userGridItems.map(\.user))
} }
} }
.focusSection() .focusSection()
} }
.animation(.linear(duration: 0.1), value: scrollViewOffset) .animation(.linear(duration: 0.1), value: scrollViewOffset)
.background { .background {
if let splashScreenImageSource, selectUserUseSplashscreen { if selectUserUseSplashscreen, splashScreenImageSources.isNotEmpty {
ZStack { ZStack {
ImageView(splashScreenImageSource) ImageView(splashScreenImageSources)
.pipeline(.Swiftfin.local)
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.id(splashScreenImageSource) .id(splashScreenImageSources)
.transition(.opacity) .transition(.opacity)
.animation(.linear, value: splashScreenImageSource) .animation(.linear, value: splashScreenImageSources)
Color.black Color.black
.opacity(0.9) .opacity(0.9)
@ -339,25 +272,6 @@ struct SelectUserView: View {
serverSelection = .all serverSelection = .all
} }
} }
setSplashScreenImageSource()
}
// MARK: - Did Appear
private func didAppear() {
viewModel.send(.getServers)
setSplashScreenImageSource()
}
// MARK: - Set Splash Screen Image Source
private func setSplashScreenImageSource() {
splashScreenImageSource = makeSplashScreenImageSource(
serverSelection: serverSelection,
allServersSelection: .all
)
} }
// MARK: - Body // MARK: - Body
@ -367,38 +281,25 @@ struct SelectUserView: View {
if viewModel.servers.isEmpty { if viewModel.servers.isEmpty {
emptyView emptyView
} else { } else {
userView contentView
} }
} }
.ignoresSafeArea() .ignoresSafeArea()
.navigationBarBranding() .navigationBarBranding()
.onAppear { .onAppear {
didAppear() viewModel.send(.getServers)
} }
.onChange(of: isEditingUsers) { _, newValue in .onChange(of: isEditingUsers) {
guard !newValue else { return } guard !isEditingUsers else { return }
selectedUsers.removeAll() selectedUsers.removeAll()
} }
.onChange(of: serverSelection) { _, newValue in .onChange(of: isPresentingLocalPin) {
gridItems = makeGridItems(for: newValue) if isPresentingLocalPin {
setSplashScreenImageSource()
}
.onChange(of: isPresentingLocalPin) { _, newValue in
if newValue {
pin = "" pin = ""
} else { } else {
selectedUsers.removeAll() selectedUsers.removeAll()
} }
} }
.onChange(of: viewModel.servers) { _, _ in
gridItems = makeGridItems(for: serverSelection)
splashScreenImageSource = makeSplashScreenImageSource(
serverSelection: serverSelection,
allServersSelection: .all
)
}
.onReceive(viewModel.events) { event in .onReceive(viewModel.events) { event in
switch event { switch event {
case let .error(eventError): case let .error(eventError):

View File

@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; };
091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; };
21951AC22D9D2010002E03E0 /* AddUserBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21951AC12D9D2010002E03E0 /* AddUserBottomButton.swift */; };
21BCDEF72D9C822000E1D180 /* AddUserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21BCDEF62D9C822000E1D180 /* AddUserGridButton.swift */; };
4E01446C2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; }; 4E01446C2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; };
4E01446D2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; }; 4E01446D2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; };
4E0195E42CE0467B007844F4 /* ItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0195E32CE04678007844F4 /* ItemSection.swift */; }; 4E0195E42CE0467B007844F4 /* ItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0195E32CE04678007844F4 /* ItemSection.swift */; };
@ -607,6 +609,10 @@
E1194F4E2BEABA9100888DB6 /* NavigationBarCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1194F4D2BEABA9100888DB6 /* NavigationBarCloseButton.swift */; }; E1194F4E2BEABA9100888DB6 /* NavigationBarCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1194F4D2BEABA9100888DB6 /* NavigationBarCloseButton.swift */; };
E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1194F4F2BEB1E3000888DB6 /* StoredValues+Temp.swift */; }; E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1194F4F2BEB1E3000888DB6 /* StoredValues+Temp.swift */; };
E119696A2CC99EA9001A58BE /* ServerTaskProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11969692CC99EA9001A58BE /* ServerTaskProgressSection.swift */; }; E119696A2CC99EA9001A58BE /* ServerTaskProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11969692CC99EA9001A58BE /* ServerTaskProgressSection.swift */; };
E11982BA2DA04F9B0008FC3F /* CenteredLazyVGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11982B92DA04F9B0008FC3F /* CenteredLazyVGrid.swift */; };
E11982BB2DA05FF50008FC3F /* CenteredLazyVGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11982B92DA04F9B0008FC3F /* CenteredLazyVGrid.swift */; };
E11982D72DA0E8240008FC3F /* ConditionalMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11982D62DA0E8240008FC3F /* ConditionalMenu.swift */; };
E11982D82DA0E8240008FC3F /* ConditionalMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11982D62DA0E8240008FC3F /* ConditionalMenu.swift */; };
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
E11BDF772B8513B40045C54A /* ItemGenre.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11BDF762B8513B40045C54A /* ItemGenre.swift */; }; E11BDF772B8513B40045C54A /* ItemGenre.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11BDF762B8513B40045C54A /* ItemGenre.swift */; };
@ -874,7 +880,6 @@
E17639F82BF2E25B004DF6AB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19D41A92BF077130082B8B2 /* Keychain.swift */; }; E17639F82BF2E25B004DF6AB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19D41A92BF077130082B8B2 /* Keychain.swift */; };
E1763A252BF2F77B004DF6AB /* ScrollIfLargerThanContainerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift */; }; E1763A252BF2F77B004DF6AB /* ScrollIfLargerThanContainerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift */; };
E1763A272BF303C9004DF6AB /* ServerSelectionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */; }; E1763A272BF303C9004DF6AB /* ServerSelectionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */; };
E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A282BF3046A004DF6AB /* AddUserButton.swift */; };
E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */; }; E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */; };
E1763A642BF3C9AA004DF6AB /* ListRowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */; }; E1763A642BF3C9AA004DF6AB /* ListRowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */; };
E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A692BF3D177004DF6AB /* PublicUserButton.swift */; }; E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A692BF3D177004DF6AB /* PublicUserButton.swift */; };
@ -1281,6 +1286,8 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; }; 091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; };
12C80CEDC871A21D98141BBE /* VideoRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRangeType.swift; sourceTree = "<group>"; }; 12C80CEDC871A21D98141BBE /* VideoRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRangeType.swift; sourceTree = "<group>"; };
21951AC12D9D2010002E03E0 /* AddUserBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserBottomButton.swift; sourceTree = "<group>"; };
21BCDEF62D9C822000E1D180 /* AddUserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserGridButton.swift; sourceTree = "<group>"; };
4E01446B2D0292E000193038 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; }; 4E01446B2D0292E000193038 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
4E0195E32CE04678007844F4 /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = "<group>"; }; 4E0195E32CE04678007844F4 /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = "<group>"; };
4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = "<group>"; }; 4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = "<group>"; };
@ -1740,6 +1747,8 @@
E1194F4D2BEABA9100888DB6 /* NavigationBarCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarCloseButton.swift; sourceTree = "<group>"; }; E1194F4D2BEABA9100888DB6 /* NavigationBarCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarCloseButton.swift; sourceTree = "<group>"; };
E1194F4F2BEB1E3000888DB6 /* StoredValues+Temp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredValues+Temp.swift"; sourceTree = "<group>"; }; E1194F4F2BEB1E3000888DB6 /* StoredValues+Temp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredValues+Temp.swift"; sourceTree = "<group>"; };
E11969692CC99EA9001A58BE /* ServerTaskProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskProgressSection.swift; sourceTree = "<group>"; }; E11969692CC99EA9001A58BE /* ServerTaskProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskProgressSection.swift; sourceTree = "<group>"; };
E11982B92DA04F9B0008FC3F /* CenteredLazyVGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenteredLazyVGrid.swift; sourceTree = "<group>"; };
E11982D62DA0E8240008FC3F /* ConditionalMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalMenu.swift; sourceTree = "<group>"; };
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; }; E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
E11BDF762B8513B40045C54A /* ItemGenre.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemGenre.swift; sourceTree = "<group>"; }; E11BDF762B8513B40045C54A /* ItemGenre.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemGenre.swift; sourceTree = "<group>"; };
E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedCaseIterable.swift; sourceTree = "<group>"; }; E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedCaseIterable.swift; sourceTree = "<group>"; };
@ -1876,7 +1885,6 @@
E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinatable.swift; sourceTree = "<group>"; }; E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinatable.swift; sourceTree = "<group>"; };
E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugSettingsView.swift; sourceTree = "<group>"; }; E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugSettingsView.swift; sourceTree = "<group>"; };
E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionMenu.swift; sourceTree = "<group>"; }; E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionMenu.swift; sourceTree = "<group>"; };
E1763A282BF3046A004DF6AB /* AddUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserButton.swift; sourceTree = "<group>"; };
E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGridButton.swift; sourceTree = "<group>"; }; E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGridButton.swift; sourceTree = "<group>"; };
E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowButton.swift; sourceTree = "<group>"; }; E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowButton.swift; sourceTree = "<group>"; };
E1763A692BF3D177004DF6AB /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = "<group>"; }; E1763A692BF3D177004DF6AB /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = "<group>"; };
@ -4688,9 +4696,10 @@
E164A8132BE4995800A54B18 /* Components */ = { E164A8132BE4995800A54B18 /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1763A282BF3046A004DF6AB /* AddUserButton.swift */, 21951AC12D9D2010002E03E0 /* AddUserBottomButton.swift */,
E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */, 21BCDEF62D9C822000E1D180 /* AddUserGridButton.swift */,
BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */, BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */,
E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */,
E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */, E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */,
); );
path = Components; path = Components;
@ -5122,10 +5131,13 @@
children = ( children = (
E102314C2BCF8A7E009D71FC /* AlternateLayoutView.swift */, E102314C2BCF8A7E009D71FC /* AlternateLayoutView.swift */,
E104DC952B9E7E29008F506D /* AssertionFailureView.swift */, E104DC952B9E7E29008F506D /* AssertionFailureView.swift */,
B65CB977628965AA9099742F /* AttributeBadge.swift */,
E18E0203288749200022598C /* BlurView.swift */, E18E0203288749200022598C /* BlurView.swift */,
E145EB212BDCCA43003BF6F3 /* BulletedList.swift */, E145EB212BDCCA43003BF6F3 /* BulletedList.swift */,
E11982B92DA04F9B0008FC3F /* CenteredLazyVGrid.swift */,
4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */, 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */,
E1A1528728FD229500600579 /* ChevronButton.swift */, E1A1528728FD229500600579 /* ChevronButton.swift */,
E11982D62DA0E8240008FC3F /* ConditionalMenu.swift */,
E1153DCB2BBB633B00424D36 /* FastSVGView.swift */, E1153DCB2BBB633B00424D36 /* FastSVGView.swift */,
531AC8BE26750DE20091C7EB /* ImageView.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */,
4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */, 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */,
@ -5145,7 +5157,6 @@
4E7315722D14752400EA2A95 /* UserProfileImage */, 4E7315722D14752400EA2A95 /* UserProfileImage */,
E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */, E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */,
E1B5784028F8AFCB00D42911 /* WrappedView.swift */, E1B5784028F8AFCB00D42911 /* WrappedView.swift */,
B65CB977628965AA9099742F /* AttributeBadge.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@ -5954,6 +5965,7 @@
files = ( files = (
E1AEFA392BE36C4C00CFAFD8 /* SwiftfinStore+ServerState.swift in Sources */, E1AEFA392BE36C4C00CFAFD8 /* SwiftfinStore+ServerState.swift in Sources */,
E15D4F0B2B1BD88900442DB8 /* Edge.swift in Sources */, E15D4F0B2B1BD88900442DB8 /* Edge.swift in Sources */,
21BCDEF72D9C822000E1D180 /* AddUserGridButton.swift in Sources */,
E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */, E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */,
E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */, E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */,
E145EB262BE055AD003BF6F3 /* ServerResponse.swift in Sources */, E145EB262BE055AD003BF6F3 /* ServerResponse.swift in Sources */,
@ -5966,6 +5978,7 @@
4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */, 4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */,
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */, 4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */,
4E5EE5532D67CFAB00982290 /* ImageCard.swift in Sources */, 4E5EE5532D67CFAB00982290 /* ImageCard.swift in Sources */,
E11982D82DA0E8240008FC3F /* ConditionalMenu.swift in Sources */,
4E661A2B2CEFE6F400025C99 /* Video3DFormat.swift in Sources */, 4E661A2B2CEFE6F400025C99 /* Video3DFormat.swift in Sources */,
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */,
@ -6015,7 +6028,6 @@
E1575EA1293E7B1E001665B1 /* String.swift in Sources */, E1575EA1293E7B1E001665B1 /* String.swift in Sources */,
4E699BC02CB3477D007CBD5D /* HomeSection.swift in Sources */, 4E699BC02CB3477D007CBD5D /* HomeSection.swift in Sources */,
E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */, E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */,
E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */,
E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */, E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */,
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */, E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */,
4E661A2F2CEFE77700025C99 /* MetadataField.swift in Sources */, 4E661A2F2CEFE77700025C99 /* MetadataField.swift in Sources */,
@ -6072,6 +6084,7 @@
BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */, BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */,
4E0A8FFC2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */, 4E0A8FFC2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */,
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
E11982BB2DA05FF50008FC3F /* CenteredLazyVGrid.swift in Sources */,
C46DD8D32A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift in Sources */, C46DD8D32A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift in Sources */,
E1549661296CA2EF00C4EF88 /* SwiftfinDefaults.swift in Sources */, E1549661296CA2EF00C4EF88 /* SwiftfinDefaults.swift in Sources */,
E158C8D12A31947500C527C5 /* MediaSourceInfoView.swift in Sources */, E158C8D12A31947500C527C5 /* MediaSourceInfoView.swift in Sources */,
@ -6186,6 +6199,7 @@
E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */, E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */,
4E2AC4CF2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift in Sources */, 4E2AC4CF2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift in Sources */,
E1D37F492B9C648E00343D2B /* MaxHeightText.swift in Sources */, E1D37F492B9C648E00343D2B /* MaxHeightText.swift in Sources */,
21951AC22D9D2010002E03E0 /* AddUserBottomButton.swift in Sources */,
E146A9DC2BE6E9BF0034DA1E /* StoredValues+User.swift in Sources */, E146A9DC2BE6E9BF0034DA1E /* StoredValues+User.swift in Sources */,
4E5D3EC82D8920AF003E2772 /* TrailerMenu.swift in Sources */, 4E5D3EC82D8920AF003E2772 /* TrailerMenu.swift in Sources */,
E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */, E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */,
@ -6438,6 +6452,7 @@
4E661A232CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */, 4E661A232CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */,
E170D0E2294CC8000017224C /* VideoPlayer+Actions.swift in Sources */, E170D0E2294CC8000017224C /* VideoPlayer+Actions.swift in Sources */,
4E10C8112CC030CD0012CC9F /* DeviceDetailsView.swift in Sources */, 4E10C8112CC030CD0012CC9F /* DeviceDetailsView.swift in Sources */,
E11982D72DA0E8240008FC3F /* ConditionalMenu.swift in Sources */,
E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */, E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */, 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */,
4E49DEE42CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */, 4E49DEE42CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */,
@ -6989,6 +7004,7 @@
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */, E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
4E1A39342D56C84200BAC1C7 /* ItemViewAttributes.swift in Sources */, 4E1A39342D56C84200BAC1C7 /* ItemViewAttributes.swift in Sources */,
4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */, 4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */,
E11982BA2DA04F9B0008FC3F /* CenteredLazyVGrid.swift in Sources */,
4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */, 4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */,
DFB7C3DF2C7AA43A00CE7CDC /* UserSignInState.swift in Sources */, DFB7C3DF2C7AA43A00CE7CDC /* UserSignInState.swift in Sources */,
E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */, E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */,

View File

@ -259,7 +259,7 @@
"location" : "https://github.com/LePips/VLCUI", "location" : "https://github.com/LePips/VLCUI",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "50d4f6ec05a2d8333952def0d8e45019a4207132" "revision" : "9e0285b13c666aace61835e8a512512edee822a6"
} }
}, },
{ {