[tvOS] Login Flow Cleanup - Second Pass (#1403)

* Background on Server User Signin. Button Sizing. More visible deletion notice. Menu ListView insets.

* wip

* Change Highlight. Move Add User Button. Remove Add User inline option.

* Take 2

* Undo user changes.

* Remove all changes.

* "selectServer" = "Select Server";

* Recommendations

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>

* Update ServerDetailView.swift

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>

* Update ServerDetailView.swift

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>

* build strings

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-01-23 20:25:08 -07:00 committed by GitHub
parent 757ea4d475
commit b0b604c4ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 185 additions and 127 deletions

View File

@ -1130,6 +1130,8 @@ internal enum L10n {
internal static let selectAll = L10n.tr("Localizable", "selectAll", fallback: "Select All")
/// Select Image
internal static let selectImage = L10n.tr("Localizable", "selectImage", fallback: "Select Image")
/// Select server
internal static let selectServer = L10n.tr("Localizable", "selectServer", fallback: "Select server")
/// Series
internal static let series = L10n.tr("Localizable", "series", fallback: "Series")
/// Series Backdrop

View File

@ -11,10 +11,12 @@ import SwiftUI
struct ListRowButton: View {
let title: String
let role: ButtonRole?
let action: () -> Void
init(_ title: String, action: @escaping () -> Void) {
init(_ title: String, role: ButtonRole? = nil, action: @escaping () -> Void) {
self.title = title
self.role = role
self.action = action
}
@ -23,15 +25,33 @@ struct ListRowButton: View {
action()
} label: {
ZStack {
Rectangle()
.foregroundStyle(.secondary)
RoundedRectangle(cornerRadius: 10)
.fill(secondaryStyle)
Text(title)
.foregroundStyle(primaryStyle)
.font(.body.weight(.bold))
.foregroundStyle(.primary)
}
}
.buttonStyle(.card)
.frame(height: 75)
}
// MARK: - Styles
private var primaryStyle: some ShapeStyle {
if role == .destructive {
return AnyShapeStyle(Color.red)
} else {
return AnyShapeStyle(.primary)
}
}
private var secondaryStyle: some ShapeStyle {
if role == .destructive {
return AnyShapeStyle(Color.red.opacity(0.2))
} else {
return AnyShapeStyle(.secondary)
}
}
}

View File

@ -22,6 +22,10 @@ struct SplitLoginWindowView<Leading: View, Trailing: View>: View {
private let trailingTitle: String
private let trailingContentView: () -> Trailing
// MARK: - Background Variable
private let backgroundImageSource: ImageSource?
// MARK: - Body
var body: some View {
@ -52,6 +56,21 @@ struct SplitLoginWindowView<Leading: View, Trailing: View>: View {
}
}
.navigationBarBranding(isLoading: isLoading)
.background {
if let backgroundImageSource {
ZStack {
ImageView(backgroundImageSource)
.aspectRatio(contentMode: .fill)
.id(backgroundImageSource)
.transition(.opacity)
.animation(.linear, value: backgroundImageSource)
Color.black
.opacity(0.9)
}
.ignoresSafeArea()
}
}
}
}
@ -61,6 +80,7 @@ extension SplitLoginWindowView {
isLoading: Bool = false,
leadingTitle: String,
trailingTitle: String,
backgroundImageSource: ImageSource? = nil,
@ViewBuilder leadingContentView: @escaping () -> Leading,
@ViewBuilder trailingContentView: @escaping () -> Trailing
) {
@ -69,5 +89,6 @@ extension SplitLoginWindowView {
self.trailingTitle = trailingTitle
self.leadingContentView = leadingContentView
self.trailingContentView = trailingContentView
self.backgroundImageSource = backgroundImageSource
}
}

View File

@ -42,13 +42,8 @@ extension SelectUserView {
self.servers = servers
}
var body: some View {
VStack {
Button {
if let selectedServer {
action(selectedServer)
}
} label: {
@ViewBuilder
private var content: some View {
ZStack {
Color.secondarySystemFill
@ -65,14 +60,41 @@ extension SelectUserView {
.foregroundStyle(isEnabled ? .primary : .secondary)
if serverSelection == .all {
Text(L10n.hidden)
// 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)
.disabled(!isEnabled)
}
}
}

View File

@ -31,11 +31,24 @@ extension SelectUserView {
@ViewBuilder
private var advancedMenu: some View {
Menu(L10n.advanced, systemImage: "gearshape.fill") {
Menu {
Button(L10n.editUsers, systemImage: "person.crop.circle") {
isEditing.toggle()
}
// TODO: Advanced settings on tvOS?
//
// Divider()
//
// Button(L10n.advanced, systemImage: "gearshape.fill") {
// router.route(to: \.advancedSettings)
// }
} label: {
Label(L10n.advanced, systemImage: "gearshape.fill")
.font(.body.weight(.semibold))
.foregroundStyle(Color.primary)
.labelStyle(.iconOnly)
.frame(width: 50, height: 50)
}
// TODO: Do we want to support a grid view and list view like iOS?
// if !viewModel.servers.isEmpty {
@ -51,40 +64,20 @@ extension SelectUserView {
// }
// .pickerStyle(.menu)
// }
}
// TODO: Advanced settings on tvOS?
// Section {
// Button(L10n.advanced, systemImage: "gearshape.fill") {
// router.route(to: \.advancedSettings)
// }
// }
}
.labelStyle(.iconOnly)
}
// MARK: - Delete User Button
private var deleteUsersButton: some View {
Button {
ListRowButton(L10n.delete, role: .destructive) {
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))
}
.frame(width: 400, height: 50)
.disabled(!areUsersSelected)
.buttonStyle(.card)
}
// MARK: - Initializer
init(
isEditing: Binding<Bool>,
serverSelection: Binding<SelectUserServerSelection>,
@ -103,6 +96,8 @@ extension SelectUserView {
self.toggleAllUsersSelected = toggleAllUsersSelected
}
// MARK: - Content View
@ViewBuilder
private var contentView: some View {
HStack(alignment: .center) {
@ -113,16 +108,20 @@ extension SelectUserView {
toggleAllUsersSelected()
} label: {
Text(areUsersSelected ? L10n.removeAll : L10n.selectAll)
.font(.body.weight(.semibold))
.foregroundStyle(Color.primary)
.font(.body.weight(.semibold))
.frame(width: 200, height: 50)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
Button {
isEditing = false
} label: {
L10n.cancel.text
.font(.body.weight(.semibold))
Text(L10n.cancel)
.foregroundStyle(Color.primary)
.font(.body.weight(.semibold))
.frame(width: 200, height: 50)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
} else {
ServerSelectionMenu(
@ -130,12 +129,12 @@ extension SelectUserView {
viewModel: viewModel
)
if userCount > 1 {
advancedMenu
}
}
}
}
// MARK: - Body
var body: some View {
// `Menu` with custom label has some weird additional

View File

@ -28,6 +28,8 @@ extension SelectUserView {
private let action: () -> Void
private let onDelete: () -> Void
// MARK: - Initializer
init(
user: UserState,
server: ServerState,
@ -42,49 +44,52 @@ extension SelectUserView {
self.onDelete = onDelete
}
// MARK: - Label Foreground Style
private var labelForegroundStyle: some ShapeStyle {
guard isEditing else { return .primary }
return isSelected ? .primary : .secondary
}
@ViewBuilder
private var personView: some View {
ZStack {
Color.secondarySystemFill
// MARK: - User Portrait
RelativeSystemImageView(systemName: "person.fill", ratio: 0.5)
.foregroundStyle(.secondary)
}
.clipShape(.circle)
.aspectRatio(1, contentMode: .fill)
}
@ViewBuilder
private var userImage: some View {
private var userPortrait: some View {
UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
),
pipeline: .Swiftfin.local
)
)
.aspectRatio(1, contentMode: .fill)
.overlay {
if isEditing {
ZStack(alignment: .bottom) {
Color.black
.opacity(isSelected ? 0 : 0.5)
.clipShape(.circle)
if isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 75, height: 75)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
}
}
}
}
}
// MARK: - Body
var body: some View {
VStack {
Button {
action()
} label: {
userImage
userPortrait
.hoverEffect(.highlight)
Text(user.username)
@ -107,16 +112,6 @@ extension SelectUserView {
}
}
}
.overlay {
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)
}
}
}
}
}

View File

@ -306,17 +306,6 @@ struct SelectUserView: View {
@ViewBuilder
private var emptyView: some View {
ZStack {
VStack {
Image(.jellyfinBlobBlue)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 100)
.edgePadding()
Color.clear
}
VStack(spacing: 50) {
L10n.connectToJellyfinServerStart.text
.font(.body)
@ -335,7 +324,6 @@ struct SelectUserView: View {
.buttonStyle(.card)
}
}
}
// MARK: - Functions
@ -350,13 +338,20 @@ struct SelectUserView: View {
}
}
// change splash screen selection if necessary
// selectUserAllServersSplashscreen = serverSelection
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
@ -385,10 +380,7 @@ struct SelectUserView: View {
.onChange(of: serverSelection) { _, newValue in
gridItems = makeGridItems(for: newValue)
splashScreenImageSource = makeSplashScreenImageSource(
serverSelection: newValue,
allServersSelection: .all
)
setSplashScreenImageSource()
}
.onChange(of: isPresentingLocalPin) { _, newValue in
if newValue {

View File

@ -75,14 +75,17 @@ struct EditServerView: View {
.foregroundColor(.secondary)
}
}
.listRowBackground(Color.clear)
.listRowInsets(.zero)
}
if isEditing {
Section {
ListRowButton(L10n.delete) {
ListRowButton(L10n.delete, role: .destructive) {
isPresentingConfirmDeletion = true
}
.foregroundStyle(.primary, .red.opacity(0.5))
.listRowBackground(Color.clear)
.listRowInsets(.zero)
}
}
}

View File

@ -166,7 +166,8 @@ struct UserSignInView: View {
SplitLoginWindowView(
isLoading: viewModel.state == .signingIn,
leadingTitle: L10n.signInToServer(viewModel.server.name),
trailingTitle: L10n.publicUsers
trailingTitle: L10n.publicUsers,
backgroundImageSource: viewModel.server.splashScreenImageSource()
) {
signInSection
} trailingContentView: {

View File

@ -80,7 +80,7 @@ extension SelectUserView {
if serverSelection == .all {
Menu {
Text("Select Server")
Text(L10n.selectServer)
ForEach(servers) { server in
Button {

View File

@ -99,7 +99,7 @@ extension SelectUserView {
if serverSelection == .all {
Menu {
Text("Select Server")
Text(L10n.selectServer)
ForEach(servers) { server in
Button {

View File

@ -1618,6 +1618,9 @@
/// Select Image
"selectImage" = "Select Image";
/// Select server
"selectServer" = "Select server";
/// Series
"series" = "Series";