Skip to content

Commit

Permalink
[tvOS] Delete User from User Selection Screen (jellyfin#1359)
Browse files Browse the repository at this point in the history
* Extract handlers into function

* Color Improvements to move away from UIColor

* Bring over edit user feature from iOS

* Fix UserGridButton overlay when editing

* Move advanced menu to be near server select menu

* Re-enable context menu

* Add bottom button bar

* hook up user deletion

* improvements

* Refactor buttons for highlight hover effect

* Pass in user count

* Don't cancel editing if delete alert is cancelled

* cleanup

* Pad bottom of buttons

* Cancel editing after user deletion

* Revert ServerSelectionMenu back to button

* Remove padding that pushed the server selection menu up too far

* Make delete button red to match iOS

* Update SelectUserView.swift

* workaround Menu layout issues

* Bring select/deselect all users behavior from iOS

* Fixes after merge with main

* Fix vertical focus

---------

Co-authored-by: chickdan <=>
Co-authored-by: Ethan Pippin <[email protected]>
  • Loading branch information
chickdan and LePips authored Dec 31, 2024
1 parent 486995b commit cd94142
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 114 deletions.
6 changes: 3 additions & 3 deletions Shared/Extensions/Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ extension Color {

// TODO: Correct and add colors
#if os(tvOS) // tvOS doesn't have these
static let systemFill = Color(UIColor.white)
static let secondarySystemFill = Color(UIColor.gray)
static let tertiarySystemFill = Color(UIColor.black)
static let systemFill = Color.white
static let secondarySystemFill = Color.gray
static let tertiarySystemFill = Color.black
static let lightGray = Color(UIColor.lightGray)

#else
Expand Down
2 changes: 1 addition & 1 deletion Shared/Extensions/ViewExtensions/Backport/Backport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ extension Backport where Content: View {
extension ButtonBorderShape {

static let circleBackport: ButtonBorderShape = {
if #available(iOS 17, tvOS 16.4, *) {
if #available(iOS 17, *) {
return ButtonBorderShape.circle
} else {
return ButtonBorderShape.roundedRectangle
Expand Down
25 changes: 13 additions & 12 deletions Swiftfin tvOS/Views/SelectUserView/Components/AddUserButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,22 @@ extension SelectUserView {
}
.clipShape(.circle)
.aspectRatio(1, contentMode: .fill)
}
.buttonStyle(.card)
.buttonBorderShape(.circleBackport)
.disabled(!isEnabled)
.hoverEffect(.highlight)

Text(L10n.addUser)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(isEnabled ? .primary : .secondary)
Text(L10n.addUser)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(isEnabled ? .primary : .secondary)

if serverSelection == .all {
Text(L10n.hidden)
.font(.footnote)
.hidden()
if serverSelection == .all {
Text(L10n.hidden)
.font(.footnote)
.hidden()
}
}
.buttonStyle(.borderless)
.buttonBorderShape(.circle)
.disabled(!isEnabled)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// 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 SwiftUI

extension SelectUserView {

struct SelectUserBottomBar: View {

@Binding
private var isEditing: Bool

@Binding
private var serverSelection: SelectUserServerSelection

@ObservedObject
private var viewModel: SelectUserViewModel

private let areUsersSelected: Bool
private let userCount: Int

private let onDelete: () -> Void
private let toggleAllUsersSelected: () -> Void

// MARK: - Advanced Menu

@ViewBuilder
private var advancedMenu: some View {
Menu(L10n.advanced, systemImage: "gearshape.fill") {

Button(L10n.editUsers, systemImage: "person.crop.circle") {
isEditing.toggle()
}

// TODO: Do we want to support a grid view and list view like iOS?
// if !viewModel.servers.isEmpty {
// Picker(selection: $userListDisplayType) {
// ForEach(LibraryDisplayType.allCases, id: \.hashValue) {
// Label($0.displayTitle, systemImage: $0.systemImage)
// .tag($0)
// }
// } label: {
// Text(L10n.layout)
// Text(userListDisplayType.displayTitle)
// Image(systemName: userListDisplayType.systemImage)
// }
// .pickerStyle(.menu)
// }

// TODO: Advanced settings on tvOS?
// Section {
// Button(L10n.advanced, systemImage: "gearshape.fill") {
// router.route(to: \.advancedSettings)
// }
// }
}
.labelStyle(.iconOnly)
}

private var deleteUsersButton: some View {
Button {
onDelete()
} label: {
ZStack {
Color.red

Text(L10n.delete)
.font(.body.weight(.semibold))
.foregroundStyle(areUsersSelected ? .primary : .secondary)

if !areUsersSelected {
Color.black
.opacity(0.5)
}
}
.frame(width: 400, height: 65)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.disabled(!areUsersSelected)
.buttonStyle(.card)
}

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
}

@ViewBuilder
private var contentView: some View {
HStack(alignment: .center) {
if isEditing {
deleteUsersButton

Button {
toggleAllUsersSelected()
} label: {
Text(areUsersSelected ? L10n.removeAll : L10n.selectAll)
.font(.body.weight(.semibold))
.foregroundStyle(Color.primary)
}

Button {
isEditing = false
} label: {
L10n.cancel.text
.font(.body.weight(.semibold))
.foregroundStyle(Color.primary)
}
} else {
ServerSelectionMenu(
selection: $serverSelection,
viewModel: viewModel
)

if userCount > 1 {
advancedMenu
}
}
}
}

var body: some View {
// `Menu` with custom label has some weird additional
// frame/padding that differs from default label style
AlternateLayoutView(alignment: .top) {
Color.clear
.frame(height: 100)
} content: {
contentView
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,9 @@ extension SelectUserView {
}
.font(.body.weight(.semibold))
.foregroundStyle(Color.primary)
.frame(height: 50)
.frame(maxWidth: 400)
.clipShape(RoundedRectangle(cornerRadius: 10))
.frame(width: 400, height: 50)
}
.menuOrder(.fixed)
.padding()
}
}
}
91 changes: 45 additions & 46 deletions Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,62 +60,61 @@ extension SelectUserView {
.aspectRatio(1, contentMode: .fill)
}

@ViewBuilder
private var userImage: some View {
UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
)
)
.aspectRatio(1, contentMode: .fill)
.overlay {
if isEditing {
Color.black
.opacity(isSelected ? 0 : 0.5)
.clipShape(.circle)
}
}
}

var body: some View {
VStack {
Button {
action()
} label: {
VStack(alignment: .center) {
ZStack {
Color.clear

UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
)
)
}
.aspectRatio(1, contentMode: .fill)
userImage
.hoverEffect(.highlight)

Text(user.username)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(labelForegroundStyle)
.lineLimit(1)

if showServer {
Text(server.name)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.buttonStyle(.card)
.buttonBorderShape(.circleBackport)
// .contextMenu {
// Button(L10n.delete, role: .destructive) {
// onDelete()
// }
// }

Text(user.username)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(labelForegroundStyle)
.lineLimit(1)

if showServer {
Text(server.name)
.font(.footnote)
.foregroundStyle(.secondary)
.buttonStyle(.borderless)
.buttonBorderShape(.circle)
.contextMenu {
Button("Delete", role: .destructive) {
onDelete()
}
}
}
.overlay {
if isEditing {
ZStack(alignment: .bottomTrailing) {
Color.black
.opacity(isSelected ? 0 : 0.5)
.clipShape(.circle)

if isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40, alignment: .bottomTrailing)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
}
}
if isEditing && isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40, alignment: .bottomTrailing)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
}
}
}
Expand Down
Loading

0 comments on commit cd94142

Please sign in to comment.