From 9250dc650dc217c82898fb314b49eefe7d0f70a0 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Sun, 10 Jul 2022 10:50:03 +0200 Subject: [PATCH 1/7] Add known users to login screen --- Shared/Generated/Strings.swift | 2 + Shared/ViewModels/UserSignInViewModel.swift | 20 ++++++ Shared/Views/UserLoginCellView.swift | 70 ++++++++++++++++++++ Swiftfin.xcodeproj/project.pbxproj | 6 ++ Swiftfin/Views/UserSignInView.swift | 15 ++++- Translations/en.lproj/Localizable.strings | Bin 12878 -> 12938 bytes 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 Shared/Views/UserLoginCellView.swift diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index a9fe5576..62f1ced4 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -138,6 +138,8 @@ internal enum L10n { internal static func jumpLengthSeconds(_ p1: UnsafePointer) -> String { return L10n.tr("Localizable", "jumpLengthSeconds", p1) } + /// Known users + internal static var knownUsers: String { return L10n.tr("Localizable", "knownUsers") } /// Larger internal static var larger: String { return L10n.tr("Localizable", "larger") } /// Largest diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index a9b154dc..c6e4ea7d 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -9,12 +9,16 @@ import CoreStore import Foundation import Stinsen +import JellyfinAPI final class UserSignInViewModel: ViewModel { @RouterObject var router: UserSignInCoordinator.Router? let server: SwiftfinStore.State.Server + + @Published + var users: [UserDto] = [] init(server: SwiftfinStore.State.Server) { self.server = server @@ -48,4 +52,20 @@ final class UserSignInViewModel: ViewModel { self.isLoading = false } + + func loadUsers() { + // TODO: this is a hack + JellyfinAPIAPI.basePath = server.currentURI + UserAPI.getPublicUsers() + .sink(receiveCompletion: { completion in + switch completion { + case .finished: () + case .failure: + self.users = [] + } + }, receiveValue: { response in + self.users = response + }) + .store(in: &cancellables) + } } diff --git a/Shared/Views/UserLoginCellView.swift b/Shared/Views/UserLoginCellView.swift new file mode 100644 index 00000000..c4daa49f --- /dev/null +++ b/Shared/Views/UserLoginCellView.swift @@ -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) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI +import JellyfinAPI + +struct UserLoginCellView: View { + + @State + private var expanded = false; + + @State + private var enteredPassword: String = "" + + var user: UserDto + var baseURL: String? + var loginTapped : (String, String) -> Void + var cancelTapped: () -> Void + + var body: some View { + DisclosureGroup() { + VStack(alignment: .leading, spacing: 16) { + SecureField(L10n.password, text: $enteredPassword) + Button { + loginTapped(user.name ?? "", enteredPassword) + } label: { + L10n.signIn.text + } + } + .padding(.leading, -16) + } label: { + HStack(spacing: 4.0) { + AsyncImage( + url: getProfileImageUrl(), + content: { image in + image.resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 50, maxHeight: 50) + .clipShape(Circle()) + }, + placeholder: { + Image(systemName: "person.circle") + .resizable() + .font(.system(size: 40)) + .scaledToFit() + .frame(maxWidth: 50, maxHeight: 50) + }) + .padding(.vertical, 4.0) + + Text(user.name ?? "") + .padding(.leading, 4.0) + Spacer() + } + } + } + + func getProfileImageUrl() -> URL? { + if let userId = user.id, let imageTag = user.primaryImageTag, let server = baseURL { + let url = URL(string: "\(server)/Users/\(userId)/Images/Primary?width=200&tag=\(imageTag)&quality=90") + LogManager.log.debug(url?.absoluteString ?? "") + return url + } + return nil + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 50d35fd9..da969d03 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -249,6 +249,8 @@ 62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; + 631759CF2879DB6A00A621AD /* UserLoginCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631759CE2879DB6A00A621AD /* UserLoginCellView.swift */; }; + 631759D02879DB6A00A621AD /* UserLoginCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631759CE2879DB6A00A621AD /* UserLoginCellView.swift */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */; }; C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; }; @@ -743,6 +745,7 @@ 62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; + 631759CE2879DB6A00A621AD /* UserLoginCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLoginCellView.swift; sourceTree = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = ""; }; C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemWideElement.swift; sourceTree = ""; }; @@ -1804,6 +1807,7 @@ E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */, 624C21742685CF60007F1390 /* SearchablePickerView.swift */, 53DE4BD1267098F300739748 /* SearchBarView.swift */, + 631759CE2879DB6A00A621AD /* UserLoginCellView.swift */, ); path = Views; sourceTree = ""; @@ -2256,6 +2260,7 @@ E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */, + 631759D02879DB6A00A621AD /* UserLoginCellView.swift in Sources */, E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, E14B4142279354770016CBE5 /* LocalizedLookup.swift in Sources */, E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */, @@ -2485,6 +2490,7 @@ 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */, + 631759CF2879DB6A00A621AD /* UserLoginCellView.swift in Sources */, E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */, E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, diff --git a/Swiftfin/Views/UserSignInView.swift b/Swiftfin/Views/UserSignInView.swift index eec3f212..d365e92d 100644 --- a/Swiftfin/Views/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView.swift @@ -19,7 +19,19 @@ struct UserSignInView: View { private var password: String = "" var body: some View { - Form { + List { + #if !os(tvOS) + // DisclosureGroup not available on tvOS + if (viewModel.users.count > 0) { + Section(header: L10n.knownUsers.text) { + ForEach(viewModel.users, id: \.id) { user in + UserLoginCellView(user: user, baseURL: viewModel.server.currentURI, loginTapped: viewModel.login, cancelTapped: viewModel.cancelSignIn) + .disabled(viewModel.isLoading) + } + } + } + #endif + Section { TextField(L10n.username, text: $username) @@ -55,5 +67,6 @@ struct UserSignInView: View { } .navigationTitle(L10n.signIn) .navigationBarBackButtonHidden(viewModel.isLoading) + .onAppear(perform: viewModel.loadUsers) } } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index c5e7a5181b4332c8ce7e5b16225cb6922b457d1f..ad118a598c74307b2b57aa0fb2b8f1bb701e0cff 100644 GIT binary patch delta 68 zcmX??(v`Zw&&WoJA)6tOA)lcfNQN>LGo&&U0ZAnW1qNFnR$}mm%PKIGBFS4ba4`S? DD7_7K delta 7 OcmeB5J(se<&j Date: Sun, 10 Jul 2022 18:47:52 +0200 Subject: [PATCH 2/7] Format code --- Shared/ViewModels/UserSignInViewModel.swift | 40 +++---- Shared/Views/UserLoginCellView.swift | 113 ++++++++++---------- Swiftfin/Views/UserSignInView.swift | 28 ++--- 3 files changed, 90 insertions(+), 91 deletions(-) diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index c6e4ea7d..9b8c1560 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -8,17 +8,17 @@ import CoreStore import Foundation -import Stinsen import JellyfinAPI +import Stinsen final class UserSignInViewModel: ViewModel { @RouterObject var router: UserSignInCoordinator.Router? let server: SwiftfinStore.State.Server - - @Published - var users: [UserDto] = [] + + @Published + var users: [UserDto] = [] init(server: SwiftfinStore.State.Server) { self.server = server @@ -52,20 +52,20 @@ final class UserSignInViewModel: ViewModel { self.isLoading = false } - - func loadUsers() { - // TODO: this is a hack - JellyfinAPIAPI.basePath = server.currentURI - UserAPI.getPublicUsers() - .sink(receiveCompletion: { completion in - switch completion { - case .finished: () - case .failure: - self.users = [] - } - }, receiveValue: { response in - self.users = response - }) - .store(in: &cancellables) - } + + func loadUsers() { + // TODO: this is a hack + JellyfinAPIAPI.basePath = server.currentURI + UserAPI.getPublicUsers() + .sink(receiveCompletion: { completion in + switch completion { + case .finished: () + case .failure: + self.users = [] + } + }, receiveValue: { response in + self.users = response + }) + .store(in: &cancellables) + } } diff --git a/Shared/Views/UserLoginCellView.swift b/Shared/Views/UserLoginCellView.swift index c4daa49f..4441250f 100644 --- a/Shared/Views/UserLoginCellView.swift +++ b/Shared/Views/UserLoginCellView.swift @@ -6,65 +6,64 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // -import SwiftUI import JellyfinAPI +import SwiftUI struct UserLoginCellView: View { - - @State - private var expanded = false; - - @State - private var enteredPassword: String = "" - - var user: UserDto - var baseURL: String? - var loginTapped : (String, String) -> Void - var cancelTapped: () -> Void - - var body: some View { - DisclosureGroup() { - VStack(alignment: .leading, spacing: 16) { - SecureField(L10n.password, text: $enteredPassword) - Button { - loginTapped(user.name ?? "", enteredPassword) - } label: { - L10n.signIn.text - } - } - .padding(.leading, -16) - } label: { - HStack(spacing: 4.0) { - AsyncImage( - url: getProfileImageUrl(), - content: { image in - image.resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: 50, maxHeight: 50) - .clipShape(Circle()) - }, - placeholder: { - Image(systemName: "person.circle") - .resizable() - .font(.system(size: 40)) - .scaledToFit() - .frame(maxWidth: 50, maxHeight: 50) - }) - .padding(.vertical, 4.0) - Text(user.name ?? "") - .padding(.leading, 4.0) - Spacer() - } - } - } - - func getProfileImageUrl() -> URL? { - if let userId = user.id, let imageTag = user.primaryImageTag, let server = baseURL { - let url = URL(string: "\(server)/Users/\(userId)/Images/Primary?width=200&tag=\(imageTag)&quality=90") - LogManager.log.debug(url?.absoluteString ?? "") - return url - } - return nil - } + @State + private var expanded = false + + @State + private var enteredPassword: String = "" + + var user: UserDto + var baseURL: String? + var loginTapped: (String, String) -> Void + var cancelTapped: () -> Void + + var body: some View { + DisclosureGroup { + VStack(alignment: .leading, spacing: 16) { + SecureField(L10n.password, text: $enteredPassword) + Button { + loginTapped(user.name ?? "", enteredPassword) + } label: { + L10n.signIn.text + } + } + .padding(.leading, -16) + } label: { + HStack(spacing: 4.0) { + AsyncImage(url: getProfileImageUrl(), + content: { image in + image.resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 50, maxHeight: 50) + .clipShape(Circle()) + }, + placeholder: { + Image(systemName: "person.circle") + .resizable() + .font(.system(size: 40)) + .scaledToFit() + .frame(maxWidth: 50, maxHeight: 50) + }) + .padding(.vertical, 4.0) + + Text(user.name ?? "") + .padding(.leading, 4.0) + Spacer() + } + } + } + + func getProfileImageUrl() -> URL? { + if let userId = user.id, let imageTag = user.primaryImageTag, let server = baseURL { + let url = URL(string: "\(server)/Users/\(userId)/Images/Primary?width=200&tag=\(imageTag)&quality=90") + LogManager.log.debug(url?.absoluteString ?? "") + return url + } + return nil + } } diff --git a/Swiftfin/Views/UserSignInView.swift b/Swiftfin/Views/UserSignInView.swift index d365e92d..d5174f50 100644 --- a/Swiftfin/Views/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView.swift @@ -19,19 +19,19 @@ struct UserSignInView: View { private var password: String = "" var body: some View { - List { - #if !os(tvOS) - // DisclosureGroup not available on tvOS - if (viewModel.users.count > 0) { - Section(header: L10n.knownUsers.text) { - ForEach(viewModel.users, id: \.id) { user in - UserLoginCellView(user: user, baseURL: viewModel.server.currentURI, loginTapped: viewModel.login, cancelTapped: viewModel.cancelSignIn) - .disabled(viewModel.isLoading) - } - } - } - #endif - + List { + #if !os(tvOS) + // DisclosureGroup not available on tvOS + if !viewModel.users.isEmpty { + Section(header: L10n.knownUsers.text) { + ForEach(viewModel.users, id: \.id) { user in + UserLoginCellView(user: user, baseURL: viewModel.server.currentURI, loginTapped: viewModel.login, + cancelTapped: viewModel.cancelSignIn) + .disabled(viewModel.isLoading) + } + } + } + #endif Section { TextField(L10n.username, text: $username) @@ -67,6 +67,6 @@ struct UserSignInView: View { } .navigationTitle(L10n.signIn) .navigationBarBackButtonHidden(viewModel.isLoading) - .onAppear(perform: viewModel.loadUsers) + .onAppear(perform: viewModel.loadUsers) } } From 3d03f840e2f054cc1152f66699d0821bc4ec7b0e Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Sun, 10 Jul 2022 21:29:57 +0200 Subject: [PATCH 3/7] Remove UserLoginCellView from tvOS target --- Swiftfin.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index da969d03..a916df67 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -250,7 +250,6 @@ 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; 631759CF2879DB6A00A621AD /* UserLoginCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631759CE2879DB6A00A621AD /* UserLoginCellView.swift */; }; - 631759D02879DB6A00A621AD /* UserLoginCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631759CE2879DB6A00A621AD /* UserLoginCellView.swift */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */; }; C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; }; @@ -2260,7 +2259,6 @@ E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */, - 631759D02879DB6A00A621AD /* UserLoginCellView.swift in Sources */, E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, E14B4142279354770016CBE5 /* LocalizedLookup.swift in Sources */, E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */, From 51d30cf60ae5c13030dd31006da8646790e79188 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Mon, 11 Jul 2022 14:29:45 +0200 Subject: [PATCH 4/7] Implement suggestions --- Shared/Generated/Strings.swift | 4 +- Shared/ViewModels/UserSignInViewModel.swift | 19 +++-- Shared/Views/UserLoginCellView.swift | 69 ------------------ Swiftfin.xcodeproj/project.pbxproj | 8 +- Swiftfin/Views/PublicUserSignInCellView.swift | 44 +++++++++++ Swiftfin/Views/UserSignInView.swift | 22 +++--- Translations/en.lproj/Localizable.strings | Bin 12938 -> 12942 bytes 7 files changed, 70 insertions(+), 96 deletions(-) delete mode 100644 Shared/Views/UserLoginCellView.swift create mode 100644 Swiftfin/Views/PublicUserSignInCellView.swift diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index 62f1ced4..301d74ef 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -138,8 +138,6 @@ internal enum L10n { internal static func jumpLengthSeconds(_ p1: UnsafePointer) -> String { return L10n.tr("Localizable", "jumpLengthSeconds", p1) } - /// Known users - internal static var knownUsers: String { return L10n.tr("Localizable", "knownUsers") } /// Larger internal static var larger: String { return L10n.tr("Localizable", "larger") } /// Largest @@ -260,6 +258,8 @@ internal enum L10n { internal static var previousItem: String { return L10n.tr("Localizable", "previousItem") } /// Programs internal static var programs: String { return L10n.tr("Localizable", "programs") } + /// Public users + internal static var publicUsers: String { return L10n.tr("Localizable", "publicUsers") } /// Rated internal static var rated: String { return L10n.tr("Localizable", "rated") } /// Recently Added diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 9b8c1560..5bc4ca56 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -18,7 +18,7 @@ final class UserSignInViewModel: ViewModel { let server: SwiftfinStore.State.Server @Published - var users: [UserDto] = [] + var publicUsers: [UserDto] = [] init(server: SwiftfinStore.State.Server) { self.server = server @@ -54,18 +54,21 @@ final class UserSignInViewModel: ViewModel { } func loadUsers() { - // TODO: this is a hack JellyfinAPIAPI.basePath = server.currentURI UserAPI.getPublicUsers() .sink(receiveCompletion: { completion in - switch completion { - case .finished: () - case .failure: - self.users = [] - } + self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion) }, receiveValue: { response in - self.users = response + self.publicUsers = response }) .store(in: &cancellables) } + + func getProfileImageUrl(user: UserDto) -> URL? { + let urlString = ImageAPI.getUserImageWithRequestBuilder(userId: user.id ?? "--", + imageType: .primary, + width: 200, + quality: 90).URLString + return URL(string: urlString) + } } diff --git a/Shared/Views/UserLoginCellView.swift b/Shared/Views/UserLoginCellView.swift deleted file mode 100644 index 4441250f..00000000 --- a/Shared/Views/UserLoginCellView.swift +++ /dev/null @@ -1,69 +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) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct UserLoginCellView: View { - - @State - private var expanded = false - - @State - private var enteredPassword: String = "" - - var user: UserDto - var baseURL: String? - var loginTapped: (String, String) -> Void - var cancelTapped: () -> Void - - var body: some View { - DisclosureGroup { - VStack(alignment: .leading, spacing: 16) { - SecureField(L10n.password, text: $enteredPassword) - Button { - loginTapped(user.name ?? "", enteredPassword) - } label: { - L10n.signIn.text - } - } - .padding(.leading, -16) - } label: { - HStack(spacing: 4.0) { - AsyncImage(url: getProfileImageUrl(), - content: { image in - image.resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: 50, maxHeight: 50) - .clipShape(Circle()) - }, - placeholder: { - Image(systemName: "person.circle") - .resizable() - .font(.system(size: 40)) - .scaledToFit() - .frame(maxWidth: 50, maxHeight: 50) - }) - .padding(.vertical, 4.0) - - Text(user.name ?? "") - .padding(.leading, 4.0) - Spacer() - } - } - } - - func getProfileImageUrl() -> URL? { - if let userId = user.id, let imageTag = user.primaryImageTag, let server = baseURL { - let url = URL(string: "\(server)/Users/\(userId)/Images/Primary?width=200&tag=\(imageTag)&quality=90") - LogManager.log.debug(url?.absoluteString ?? "") - return url - } - return nil - } -} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index a9883555..1162e948 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -247,7 +247,7 @@ 62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; - 631759CF2879DB6A00A621AD /* UserLoginCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631759CE2879DB6A00A621AD /* UserLoginCellView.swift */; }; + 631759CF2879DB6A00A621AD /* PublicUserSignInCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631759CE2879DB6A00A621AD /* PublicUserSignInCellView.swift */; }; 637FCAF4287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; 637FCAF5287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; @@ -743,7 +743,7 @@ 62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; - 631759CE2879DB6A00A621AD /* UserLoginCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLoginCellView.swift; sourceTree = ""; }; + 631759CE2879DB6A00A621AD /* PublicUserSignInCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserSignInCellView.swift; sourceTree = ""; }; 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = UDPBroadcast.xcframework; path = Carthage/Build/UDPBroadcast.xcframework; sourceTree = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = ""; }; @@ -1669,6 +1669,7 @@ E1E5D54A2783E26100692DFE /* SettingsView */, E13DD3FB2717EAE8009D4DAF /* UserListView.swift */, E13DD3F4271793BB009D4DAF /* UserSignInView.swift */, + 631759CE2879DB6A00A621AD /* PublicUserSignInCellView.swift */, E193D5452719418B00900D82 /* VideoPlayer */, ); path = Views; @@ -1808,7 +1809,6 @@ E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */, 624C21742685CF60007F1390 /* SearchablePickerView.swift */, 53DE4BD1267098F300739748 /* SearchBarView.swift */, - 631759CE2879DB6A00A621AD /* UserLoginCellView.swift */, ); path = Views; sourceTree = ""; @@ -2488,7 +2488,7 @@ 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */, - 631759CF2879DB6A00A621AD /* UserLoginCellView.swift in Sources */, + 631759CF2879DB6A00A621AD /* PublicUserSignInCellView.swift in Sources */, E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */, E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, diff --git a/Swiftfin/Views/PublicUserSignInCellView.swift b/Swiftfin/Views/PublicUserSignInCellView.swift new file mode 100644 index 00000000..5771ed24 --- /dev/null +++ b/Swiftfin/Views/PublicUserSignInCellView.swift @@ -0,0 +1,44 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct UserLoginCellView: View { + + @ObservedObject + var viewModel: UserSignInViewModel + + @State + private var enteredPassword: String = "" + + var user: UserDto + + var body: some View { + DisclosureGroup { + SecureField(L10n.password, text: $enteredPassword) + Button { + viewModel.login(username: user.name ?? "--", password: enteredPassword) + } label: { + L10n.signIn.text + } + } label: { + HStack { + ImageView(viewModel.getProfileImageUrl(user: user)) { + Image(systemName: "person.circle") + .frame(width: 50, height: 50) + } + .frame(width: 50, height: 50) + .clipShape(Circle()) + + Text(user.name ?? "") + Spacer() + } + } + } +} diff --git a/Swiftfin/Views/UserSignInView.swift b/Swiftfin/Views/UserSignInView.swift index d5174f50..b31669b4 100644 --- a/Swiftfin/Views/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView.swift @@ -20,19 +20,6 @@ struct UserSignInView: View { var body: some View { List { - #if !os(tvOS) - // DisclosureGroup not available on tvOS - if !viewModel.users.isEmpty { - Section(header: L10n.knownUsers.text) { - ForEach(viewModel.users, id: \.id) { user in - UserLoginCellView(user: user, baseURL: viewModel.server.currentURI, loginTapped: viewModel.login, - cancelTapped: viewModel.cancelSignIn) - .disabled(viewModel.isLoading) - } - } - } - #endif - Section { TextField(L10n.username, text: $username) .disableAutocorrection(true) @@ -59,6 +46,15 @@ struct UserSignInView: View { } header: { L10n.signInToServer(viewModel.server.name).text } + + if !viewModel.publicUsers.isEmpty { + Section(header: L10n.publicUsers.text) { + ForEach(viewModel.publicUsers, id: \.id) { user in + UserLoginCellView(viewModel: viewModel, user: user) + .disabled(viewModel.isLoading) + } + } + } } .alert(item: $viewModel.errorMessage) { _ in Alert(title: Text(viewModel.alertTitle), diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index ad118a598c74307b2b57aa0fb2b8f1bb701e0cff..a23e6b50a3eaef730fa789696ba622dfc73f5871 100644 GIT binary patch delta 70 zcmeB5?MvMdU}RUoP|A?Rki(G4kjxOuP|T3ZPy{5E7!(+6fmn$l07+f}$VRBJX5eA~ E06^{yy#N3J delta 66 zcmeB6?MmGcU}TlekjIeEP!1$R8HyQF8H#|U5`zMREf6a)c*A8C7)p`ktr@r&010jl A6#xJL From e2291955f99043f95396ce6f28fc7d7cfc2cc8aa Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Mon, 11 Jul 2022 14:49:19 +0200 Subject: [PATCH 5/7] Allow reloading of public users --- Shared/Generated/Strings.swift | 2 ++ Shared/ViewModels/UserSignInViewModel.swift | 5 +++++ Swiftfin/Views/UserSignInView.swift | 23 ++++++++++++++++++-- Translations/en.lproj/Localizable.strings | Bin 12942 -> 13016 bytes 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index 301d74ef..9c86798e 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -198,6 +198,8 @@ internal enum L10n { internal static var `none`: String { return L10n.tr("Localizable", "none") } /// No overview available internal static var noOverviewAvailable: String { return L10n.tr("Localizable", "noOverviewAvailable") } + /// No public users + internal static var noPublicUsers: String { return L10n.tr("Localizable", "noPublicUsers") } /// No results. internal static var noResults: String { return L10n.tr("Localizable", "noResults") } /// Normal diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 5bc4ca56..412ff59c 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -17,6 +17,9 @@ final class UserSignInViewModel: ViewModel { var router: UserSignInCoordinator.Router? let server: SwiftfinStore.State.Server + @Published + var isLoadingUsers = false + @Published var publicUsers: [UserDto] = [] @@ -54,12 +57,14 @@ final class UserSignInViewModel: ViewModel { } func loadUsers() { + self.isLoadingUsers = true JellyfinAPIAPI.basePath = server.currentURI UserAPI.getPublicUsers() .sink(receiveCompletion: { completion in self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion) }, receiveValue: { response in self.publicUsers = response + self.isLoadingUsers = false }) .store(in: &cancellables) } diff --git a/Swiftfin/Views/UserSignInView.swift b/Swiftfin/Views/UserSignInView.swift index b31669b4..a4c0d0fc 100644 --- a/Swiftfin/Views/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView.swift @@ -47,12 +47,31 @@ struct UserSignInView: View { L10n.signInToServer(viewModel.server.name).text } - if !viewModel.publicUsers.isEmpty { - Section(header: L10n.publicUsers.text) { + Section { + if !viewModel.publicUsers.isEmpty { ForEach(viewModel.publicUsers, id: \.id) { user in UserLoginCellView(viewModel: viewModel, user: user) .disabled(viewModel.isLoading) } + } else { + HStack(alignment: .center) { + Spacer() + L10n.noPublicUsers.text + .font(.callout) + .foregroundColor(.secondary) + Spacer() + } + } + } header: { + HStack { + L10n.publicUsers.text + Spacer() + Button { + viewModel.loadUsers() + } label: { + Image(systemName: "arrow.clockwise.circle.fill") + } + .disabled(viewModel.isLoadingUsers || viewModel.isLoading) } } } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index a23e6b50a3eaef730fa789696ba622dfc73f5871..f41baf6ac0d596451f41bd47c8b58a49b368d14e 100644 GIT binary patch delta 29 kcmeB6y^*@1&uH=nO*Y1W$$>`l?0yXS3 Date: Mon, 11 Jul 2022 17:59:40 +0200 Subject: [PATCH 6/7] Implement suggestions --- Shared/Generated/Strings.swift | 4 ++-- Swiftfin/Views/PublicUserSignInCellView.swift | 3 ++- Swiftfin/Views/UserSignInView.swift | 1 + Translations/en.lproj/Localizable.strings | Bin 13016 -> 13016 bytes 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index 9c86798e..8b668a97 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -198,7 +198,7 @@ internal enum L10n { internal static var `none`: String { return L10n.tr("Localizable", "none") } /// No overview available internal static var noOverviewAvailable: String { return L10n.tr("Localizable", "noOverviewAvailable") } - /// No public users + /// No public Users internal static var noPublicUsers: String { return L10n.tr("Localizable", "noPublicUsers") } /// No results. internal static var noResults: String { return L10n.tr("Localizable", "noResults") } @@ -260,7 +260,7 @@ internal enum L10n { internal static var previousItem: String { return L10n.tr("Localizable", "previousItem") } /// Programs internal static var programs: String { return L10n.tr("Localizable", "programs") } - /// Public users + /// Public Users internal static var publicUsers: String { return L10n.tr("Localizable", "publicUsers") } /// Rated internal static var rated: String { return L10n.tr("Localizable", "rated") } diff --git a/Swiftfin/Views/PublicUserSignInCellView.swift b/Swiftfin/Views/PublicUserSignInCellView.swift index 5771ed24..84b10ab9 100644 --- a/Swiftfin/Views/PublicUserSignInCellView.swift +++ b/Swiftfin/Views/PublicUserSignInCellView.swift @@ -31,12 +31,13 @@ struct UserLoginCellView: View { HStack { ImageView(viewModel.getProfileImageUrl(user: user)) { Image(systemName: "person.circle") + .resizable() .frame(width: 50, height: 50) } .frame(width: 50, height: 50) .clipShape(Circle()) - Text(user.name ?? "") + Text(user.name ?? "--") Spacer() } } diff --git a/Swiftfin/Views/UserSignInView.swift b/Swiftfin/Views/UserSignInView.swift index a4c0d0fc..b2940d41 100644 --- a/Swiftfin/Views/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView.swift @@ -74,6 +74,7 @@ struct UserSignInView: View { .disabled(viewModel.isLoadingUsers || viewModel.isLoading) } } + .headerProminence(.increased) } .alert(item: $viewModel.errorMessage) { _ in Alert(title: Text(viewModel.alertTitle), diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index f41baf6ac0d596451f41bd47c8b58a49b368d14e..4471fd68383feec5d1b3205c800271ff23a0cf0e 100644 GIT binary patch delta 34 ocmcbSdLwm1oe^W`%xc~qF delta 32 mcmcbSdLwm1ozdh4nqrfu7?}u^G88kUG86$xB?fB-E(QS5;|YTR From 1e73e90d4f0eb92329711873661e8ca7c70b0522 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Mon, 11 Jul 2022 18:19:14 +0200 Subject: [PATCH 7/7] Linter --- Swiftfin/Views/PublicUserSignInCellView.swift | 2 +- Swiftfin/Views/UserSignInView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Swiftfin/Views/PublicUserSignInCellView.swift b/Swiftfin/Views/PublicUserSignInCellView.swift index 84b10ab9..184b270c 100644 --- a/Swiftfin/Views/PublicUserSignInCellView.swift +++ b/Swiftfin/Views/PublicUserSignInCellView.swift @@ -31,7 +31,7 @@ struct UserLoginCellView: View { HStack { ImageView(viewModel.getProfileImageUrl(user: user)) { Image(systemName: "person.circle") - .resizable() + .resizable() .frame(width: 50, height: 50) } .frame(width: 50, height: 50) diff --git a/Swiftfin/Views/UserSignInView.swift b/Swiftfin/Views/UserSignInView.swift index b2940d41..6c7dc759 100644 --- a/Swiftfin/Views/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView.swift @@ -74,7 +74,7 @@ struct UserSignInView: View { .disabled(viewModel.isLoadingUsers || viewModel.isLoading) } } - .headerProminence(.increased) + .headerProminence(.increased) } .alert(item: $viewModel.errorMessage) { _ in Alert(title: Text(viewModel.alertTitle),