From 6a23570d933c42af94ae13b09f100c3d4595abf0 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 9 Aug 2022 11:22:52 -0600 Subject: [PATCH] iOS/iPadOS Quick Connect (#522) --- .../QuickConnectCoordinator.swift | 30 +++++ .../Coordinators/UserSignInCoordinator.swift | 10 ++ .../UserDtoExtensions.swift | 26 ++++ Shared/Objects/RepeatingTimer.swift | 41 ++++++ Shared/ViewModels/UserSignInViewModel.swift | 51 +++----- Swiftfin tvOS/Views/UserSignInView.swift | 19 ++- Swiftfin.xcodeproj/project.pbxproj | 54 ++++++-- .../xcschemes/Swiftfin Widget.xcscheme | 123 ------------------ Swiftfin/Views/PublicUserSignInCellView.swift | 45 ------- Swiftfin/Views/QuickConnectView.swift | 55 ++++++++ .../Views/SettingsView/SettingsView.swift | 18 +-- .../Components/PublicUserSignInView.swift | 48 +++++++ .../{ => UserSignInView}/UserSignInView.swift | 20 ++- 13 files changed, 313 insertions(+), 227 deletions(-) create mode 100644 Shared/Coordinators/QuickConnectCoordinator.swift create mode 100644 Shared/Extensions/JellyfinAPIExtensions/UserDtoExtensions.swift create mode 100644 Shared/Objects/RepeatingTimer.swift delete mode 100644 Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin Widget.xcscheme delete mode 100644 Swiftfin/Views/PublicUserSignInCellView.swift create mode 100644 Swiftfin/Views/QuickConnectView.swift create mode 100644 Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift rename Swiftfin/Views/{ => UserSignInView}/UserSignInView.swift (84%) diff --git a/Shared/Coordinators/QuickConnectCoordinator.swift b/Shared/Coordinators/QuickConnectCoordinator.swift new file mode 100644 index 00000000..db79c2cf --- /dev/null +++ b/Shared/Coordinators/QuickConnectCoordinator.swift @@ -0,0 +1,30 @@ +// +// 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 Foundation +import Stinsen +import SwiftUI + +final class QuickConnectCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \QuickConnectCoordinator.start) + + @Root + var start = makeStart + + private let viewModel: UserSignInViewModel + + init(viewModel: UserSignInViewModel) { + self.viewModel = viewModel + } + + @ViewBuilder + func makeStart() -> some View { + QuickConnectView(viewModel: viewModel) + } +} diff --git a/Shared/Coordinators/UserSignInCoordinator.swift b/Shared/Coordinators/UserSignInCoordinator.swift index 5f82964c..28fe2c01 100644 --- a/Shared/Coordinators/UserSignInCoordinator.swift +++ b/Shared/Coordinators/UserSignInCoordinator.swift @@ -16,6 +16,10 @@ final class UserSignInCoordinator: NavigationCoordinatable { @Root var start = makeStart + #if !os(tvOS) + @Route(.modal) + var quickConnect = makeQuickConnect + #endif let viewModel: UserSignInViewModel @@ -23,6 +27,12 @@ final class UserSignInCoordinator: NavigationCoordinatable { self.viewModel = viewModel } + #if !os(tvOS) + func makeQuickConnect() -> NavigationViewCoordinator { + NavigationViewCoordinator(QuickConnectCoordinator(viewModel: viewModel)) + } + #endif + @ViewBuilder func makeStart() -> some View { UserSignInView(viewModel: viewModel) diff --git a/Shared/Extensions/JellyfinAPIExtensions/UserDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/UserDtoExtensions.swift new file mode 100644 index 00000000..f8bb1395 --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/UserDtoExtensions.swift @@ -0,0 +1,26 @@ +// +// 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 Foundation +import JellyfinAPI +import UIKit + +extension UserDto { + func profileImageSource(maxWidth: CGFloat, maxHeight: CGFloat) -> ImageSource { + let scaleWidth = UIScreen.main.scale(maxWidth) + let scaleHeight = UIScreen.main.scale(maxHeight) + let profileImageURL = ImageAPI.getUserImageWithRequestBuilder( + userId: id ?? "", + imageType: .primary, + maxWidth: scaleWidth, + maxHeight: scaleHeight + ).url + + return ImageSource(url: profileImageURL, blurHash: nil) + } +} diff --git a/Shared/Objects/RepeatingTimer.swift b/Shared/Objects/RepeatingTimer.swift new file mode 100644 index 00000000..5b4f6181 --- /dev/null +++ b/Shared/Objects/RepeatingTimer.swift @@ -0,0 +1,41 @@ +// +// 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 Foundation + +class RepeatingTimer { + + let action: () -> Void + private let interval: TimeInterval + private var timer: Timer? + + init(interval: TimeInterval, _ action: @escaping () -> Void) { + self.action = action + self.interval = interval + } + + @objc + private func runAction() { + action() + } + + func start() { + self.timer = Timer.scheduledTimer( + timeInterval: interval, + target: self, + selector: #selector(runAction), + userInfo: nil, + repeats: true + ) + } + + func stop() { + self.timer?.invalidate() + self.timer = nil + } +} diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 1eb6e0fd..5c927ddc 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -14,7 +14,7 @@ import Stinsen final class UserSignInViewModel: ViewModel { @RouterObject - private var Router: UserSignInCoordinator.Router? + private var router: UserSignInCoordinator.Router? @Published var publicUsers: [UserDto] = [] @@ -24,7 +24,7 @@ final class UserSignInViewModel: ViewModel { var quickConnectEnabled = false let server: SwiftfinStore.State.Server - private var quickConnectTimer: Timer? + private var quickConnectTimer: RepeatingTimer? private var quickConnectSecret: String? init(server: SwiftfinStore.State.Server) { @@ -33,6 +33,7 @@ final class UserSignInViewModel: ViewModel { JellyfinAPIAPI.basePath = server.currentURI checkQuickConnect() + getPublicUsers() } var alertTitle: String { @@ -64,7 +65,7 @@ final class UserSignInViewModel: ViewModel { self.isLoading = false } - func loadUsers() { + func getPublicUsers() { UserAPI.getPublicUsers() .trackActivity(loading) .sink(receiveCompletion: { completion in @@ -75,35 +76,17 @@ final class UserSignInViewModel: ViewModel { .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) - } - - func getSplashscreenUrl() -> URL? { - let urlString = ImageAPI.getSplashscreenWithRequestBuilder().URLString - return URL(string: urlString) - } - func checkQuickConnect() { QuickConnectAPI.getEnabled() .sink(receiveCompletion: { completion in self.handleAPIRequestError(completion: completion) }, receiveValue: { enabled in self.quickConnectEnabled = enabled - if enabled { - self.startQuickConnect() - } }) .store(in: &cancellables) } - private func startQuickConnect() { + func startQuickConnect(_ onSuccess: @escaping () -> Void) { QuickConnectAPI.initiate() .sink(receiveCompletion: { completion in self.handleAPIRequestError(completion: completion) @@ -113,19 +96,17 @@ final class UserSignInViewModel: ViewModel { self.quickConnectCode = response.code LogManager.log.debug("QuickConnect code: \(response.code ?? "--")") - self.quickConnectTimer = Timer.scheduledTimer( - timeInterval: 5, - target: self, - selector: #selector(self.checkAuthStatus), - userInfo: nil, - repeats: true - ) + self.quickConnectTimer = RepeatingTimer(interval: 5) { + self.checkAuthStatus(onSuccess) + } + + self.quickConnectTimer?.start() }) .store(in: &cancellables) } @objc - private func checkAuthStatus() { + private func checkAuthStatus(_ onSuccess: @escaping () -> Void) { guard let quickConnectSecret = quickConnectSecret else { return } QuickConnectAPI.connect(secret: quickConnectSecret) @@ -138,7 +119,8 @@ final class UserSignInViewModel: ViewModel { return } - self.quickConnectTimer?.invalidate() + self.stopQuickConnectAuthCheck() + onSuccess() SessionManager.main.signInUser(server: self.server, quickConnectSecret: quickConnectSecret) .trackActivity(self.loading) @@ -150,4 +132,11 @@ final class UserSignInViewModel: ViewModel { }) .store(in: &cancellables) } + + func stopQuickConnectAuthCheck() { + DispatchQueue.main.async { + self.quickConnectTimer?.stop() + self.quickConnectTimer = nil + } + } } diff --git a/Swiftfin tvOS/Views/UserSignInView.swift b/Swiftfin tvOS/Views/UserSignInView.swift index 29987a4b..da98671a 100644 --- a/Swiftfin tvOS/Views/UserSignInView.swift +++ b/Swiftfin tvOS/Views/UserSignInView.swift @@ -6,6 +6,7 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // +import JellyfinAPI import Stinsen import SwiftUI @@ -20,7 +21,7 @@ struct UserSignInView: View { var body: some View { ZStack { - ImageView(viewModel.getSplashscreenUrl()) + ImageView(ImageAPI.getSplashscreenWithRequestBuilder().url) .ignoresSafeArea() Color.black @@ -96,14 +97,20 @@ struct UserSignInView: View { .frame(maxWidth: .infinity) } .frame(maxWidth: .infinity) + .onAppear { + viewModel.startQuickConnect {} + } + .onDisappear { + viewModel.stopQuickConnectAuthCheck() + } } } } } } -struct UserSignInView_Preivews: PreviewProvider { - static var previews: some View { - UserSignInView(viewModel: .init(server: .sample)) - } -} +// struct UserSignInView_Preivews: PreviewProvider { +// static var previews: some View { +// UserSignInView(viewModel: .init(server: .sample)) +// } +// } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 636c898d..c41e2f70 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -182,7 +182,6 @@ 62EC3530267666A5000E9F2D /* 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 /* PublicUserSignInCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631759CE2879DB6A00A621AD /* PublicUserSignInCellView.swift */; }; 6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175A287DDFB9000603CE /* QuickConnectSettingsView.swift */; }; 6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */; }; 637FCAF5287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; @@ -254,6 +253,7 @@ E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; }; E1101177281B1E8A006A3584 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1101176281B1E8A006A3584 /* Puppy */; }; E111DE222790BB46008118A3 /* DetectBottomScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */; }; + E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */; }; E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; }; E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; }; E11895A9289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */; }; @@ -337,6 +337,11 @@ E184C161288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */; }; E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */; }; E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */; }; + E18CE0AF28A222240092E7F1 /* PublicUserSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0AE28A222240092E7F1 /* PublicUserSignInView.swift */; }; + E18CE0B228A229E70092E7F1 /* UserDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B128A229E70092E7F1 /* UserDtoExtensions.swift */; }; + E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */; }; + E18CE0B528A22EDD0092E7F1 /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */; }; + E18CE0B928A2322D0092E7F1 /* QuickConnectCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B828A2322D0092E7F1 /* QuickConnectCoordinator.swift */; }; E18E01A9288746AF0022598C /* PortraitPosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */; }; E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A4288746AF0022598C /* RefreshableScrollView.swift */; }; E18E01AB288746AF0022598C /* PillHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A5288746AF0022598C /* PillHStack.swift */; }; @@ -690,7 +695,6 @@ 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 /* PublicUserSignInCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserSignInCellView.swift; sourceTree = ""; }; 6334175A287DDFB9000603CE /* QuickConnectSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectSettingsView.swift; sourceTree = ""; }; 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectSettingsViewModel.swift; sourceTree = ""; }; 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = UDPBroadcast.xcframework; path = Carthage/Build/UDPBroadcast.xcframework; sourceTree = ""; }; @@ -742,6 +746,7 @@ E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = ""; }; E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = ""; }; E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectBottomScrollView.swift; sourceTree = ""; }; + E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectView.swift; sourceTree = ""; }; E118959C289312020042947B /* BaseItemPerson+Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemPerson+Poster.swift"; sourceTree = ""; }; E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewOffsetModifier.swift; sourceTree = ""; }; E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarOffsetModifier.swift; sourceTree = ""; }; @@ -794,6 +799,10 @@ E17885A3278105170094FBCF /* SFSymbolButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSymbolButton.swift; sourceTree = ""; }; E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilderExtensions.swift; sourceTree = ""; }; E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Poster.swift"; sourceTree = ""; }; + E18CE0AE28A222240092E7F1 /* PublicUserSignInView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicUserSignInView.swift; sourceTree = ""; }; + E18CE0B128A229E70092E7F1 /* UserDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDtoExtensions.swift; sourceTree = ""; }; + E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatingTimer.swift; sourceTree = ""; }; + E18CE0B828A2322D0092E7F1 /* QuickConnectCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectCoordinator.swift; sourceTree = ""; }; E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortraitPosterHStack.swift; sourceTree = ""; }; E18E01A4288746AF0022598C /* RefreshableScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; E18E01A5288746AF0022598C /* PillHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillHStack.swift; sourceTree = ""; }; @@ -1145,15 +1154,16 @@ children = ( E1D4BF802719D22800A11E64 /* AppAppearance.swift */, E1D4BF862719D27100A11E64 /* Bitrates.swift */, - E1C925F328875037002A7A66 /* ItemViewType.swift */, 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, E19169CD272514760085832A /* HTTPScheme.swift */, + E1C925F328875037002A7A66 /* ItemViewType.swift */, E1AA331E2782639D00F6439C /* OverlayType.swift */, E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */, E193D4DA27193CCA00900D82 /* PillStackable.swift */, E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */, E1937A60288F32DB00CB80AA /* Poster.swift */, + E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */, 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */, E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */, 535870AC2669D8DD00D05A09 /* Typings.swift */, @@ -1405,8 +1415,8 @@ E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */, E18E01A7288746AF0022598C /* DotHStack.swift */, E18E01A5288746AF0022598C /* PillHStack.swift */, - E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */, 53F866432687A45F00DCD1D7 /* PortraitPosterButton.swift */, + E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */, E1AA331C2782541500F6439C /* PrimaryButton.swift */, E18E01A4288746AF0022598C /* RefreshableScrollView.swift */, ); @@ -1425,7 +1435,6 @@ 621338912660106C00A81A2A /* Extensions */ = { isa = PBXGroup; children = ( - E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */, E1A2C157279A7D76005EC829 /* BundleExtensions.swift */, E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */, 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, @@ -1433,11 +1442,12 @@ E1399473289B1EA900401ABC /* Defaults+Workaround.swift */, E1E00A34278628A40022235B /* DoubleExtensions.swift */, E11CEB8C28999B4A003E74C7 /* FontExtensions.swift */, + E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */, 621338922660107500A81A2A /* StringExtensions.swift */, E1A2C153279A7D5A005EC829 /* UIApplicationExtensions.swift */, E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */, - E18E0239288749540022598C /* UIScrollViewExtensions.swift */, E1937A3D288F0D3D00CB80AA /* UIScreenExtensions.swift */, + E18E0239288749540022598C /* UIScrollViewExtensions.swift */, E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */, 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */, E11895A22893409D0042947B /* ViewExtensions */, @@ -1466,11 +1476,12 @@ 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */, + C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */, C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */, C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */, - C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */, E193D5412719404B00900D82 /* MainCoordinator */, C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */, + E18CE0B828A2322D0092E7F1 /* QuickConnectCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */, E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */, @@ -1544,6 +1555,15 @@ path = ItemViewModel; sourceTree = ""; }; + E1171A1A28A2215800FA1AF5 /* UserSignInView */ = { + isa = PBXGroup; + children = ( + E18CE0B028A222310092E7F1 /* Components */, + E13DD3F4271793BB009D4DAF /* UserSignInView.swift */, + ); + path = UserSignInView; + sourceTree = ""; + }; E11895A22893409D0042947B /* ViewExtensions */ = { isa = PBXGroup; children = ( @@ -1675,12 +1695,12 @@ C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */, C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */, C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */, - 631759CE2879DB6A00A621AD /* PublicUserSignInCellView.swift */, + E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */, E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */, E13DD3E427177D15009D4DAF /* ServerListView.swift */, E1E5D54A2783E26100692DFE /* SettingsView */, E13DD3FB2717EAE8009D4DAF /* UserListView.swift */, - E13DD3F4271793BB009D4DAF /* UserSignInView.swift */, + E1171A1A28A2215800FA1AF5 /* UserSignInView */, E193D5452719418B00900D82 /* VideoPlayer */, ); path = Views; @@ -1776,6 +1796,14 @@ path = Overlays; sourceTree = ""; }; + E18CE0B028A222310092E7F1 /* Components */ = { + isa = PBXGroup; + children = ( + E18CE0AE28A222240092E7F1 /* PublicUserSignInView.swift */, + ); + path = Components; + sourceTree = ""; + }; E18E01B4288747230022598C /* iPadOS */ = { isa = PBXGroup; children = ( @@ -1964,6 +1992,7 @@ E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */, E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */, + E18CE0B128A229E70092E7F1 /* UserDtoExtensions.swift */, ); path = JellyfinAPIExtensions; sourceTree = ""; @@ -2502,6 +2531,7 @@ E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */, E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */, E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */, + E18CE0B528A22EDD0092E7F1 /* RepeatingTimer.swift in Sources */, E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */, E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */, 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, @@ -2579,6 +2609,7 @@ C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */, 62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */, E18E01E8288747230022598C /* SeriesItemContentView.swift in Sources */, + E18CE0AF28A222240092E7F1 /* PublicUserSignInView.swift in Sources */, E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */, E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */, E176DE6D278E30D2001EFD8D /* EpisodeCard.swift in Sources */, @@ -2636,11 +2667,13 @@ 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, + E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, E11CEB8D28999B4A003E74C7 /* FontExtensions.swift in Sources */, E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */, E11895A9289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */, E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */, + E18CE0B228A229E70092E7F1 /* UserDtoExtensions.swift in Sources */, E18E01F0288747230022598C /* AttributeHStack.swift in Sources */, 6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */, E18E0205288749200022598C /* AppIcon.swift in Sources */, @@ -2657,6 +2690,7 @@ 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */, + E18CE0B928A2322D0092E7F1 /* QuickConnectCoordinator.swift in Sources */, 6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */, E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */, 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, @@ -2670,7 +2704,6 @@ 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */, - 631759CF2879DB6A00A621AD /* PublicUserSignInCellView.swift in Sources */, E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */, E18E01DC288747230022598C /* iPadOSCinematicScrollView.swift in Sources */, @@ -2713,6 +2746,7 @@ E176DE70278E369F001EFD8D /* MissingItemsSettingsView.swift in Sources */, C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */, E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */, + E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, 5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */, diff --git a/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin Widget.xcscheme b/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin Widget.xcscheme deleted file mode 100644 index ee1d754f..00000000 --- a/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin Widget.xcscheme +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Swiftfin/Views/PublicUserSignInCellView.swift b/Swiftfin/Views/PublicUserSignInCellView.swift deleted file mode 100644 index c82b696a..00000000 --- a/Swiftfin/Views/PublicUserSignInCellView.swift +++ /dev/null @@ -1,45 +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 { - - @ObservedObject - var viewModel: UserSignInViewModel - - @State - private var enteredPassword: String = "" - - var user: UserDto - - var body: some View { - DisclosureGroup { - SecureField(L10n.password, text: $enteredPassword) - Button { - viewModel.signIn(username: user.name ?? "--", password: enteredPassword) - } label: { - L10n.signIn.text - } - } label: { - 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 ?? "--") - Spacer() - } - } - } -} diff --git a/Swiftfin/Views/QuickConnectView.swift b/Swiftfin/Views/QuickConnectView.swift new file mode 100644 index 00000000..9ec4dd6e --- /dev/null +++ b/Swiftfin/Views/QuickConnectView.swift @@ -0,0 +1,55 @@ +// +// 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 + +struct QuickConnectView: View { + + @EnvironmentObject + private var router: QuickConnectCoordinator.Router + @ObservedObject + var viewModel: UserSignInViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + L10n.quickConnectStep1.text + + L10n.quickConnectStep2.text + + L10n.quickConnectStep3.text + .padding(.bottom) + + Text(viewModel.quickConnectCode ?? "------") + .tracking(10) + .font(.largeTitle) + .monospacedDigit() + .frame(maxWidth: .infinity) + + Spacer() + } + .padding(.horizontal) + .navigationTitle(L10n.quickConnect) + .onAppear { + viewModel.startQuickConnect { + self.router.dismissCoordinator() + } + } + .onDisappear { + viewModel.stopQuickConnectAuthCheck() + } + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + router.dismissCoordinator() + } label: { + Image(systemName: "xmark.circle.fill") + } + } + } + } +} diff --git a/Swiftfin/Views/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView.swift index e07da362..074e53bc 100644 --- a/Swiftfin/Views/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView.swift @@ -67,15 +67,6 @@ struct SettingsView: View { } } - Button { - settingsRouter.dismissCoordinator { - SessionManager.main.logout() - } - } label: { - L10n.switchUser.text - .font(.callout) - } - Button { settingsRouter.route(to: \.quickConnect) } label: { @@ -86,6 +77,15 @@ struct SettingsView: View { Image(systemName: "chevron.right") } } + + Button { + settingsRouter.dismissCoordinator { + SessionManager.main.logout() + } + } label: { + L10n.switchUser.text + .font(.callout) + } } Section(header: L10n.videoPlayer.text) { diff --git a/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift b/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift new file mode 100644 index 00000000..20fe5681 --- /dev/null +++ b/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift @@ -0,0 +1,48 @@ +// +// 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 + +extension UserSignInView { + + struct PublicUserSignInView: View { + + @ObservedObject + var viewModel: UserSignInViewModel + + @State + private var enteredPassword: String = "" + + let publicUser: UserDto + + var body: some View { + DisclosureGroup { + SecureField(L10n.password, text: $enteredPassword) + Button { + viewModel.signIn(username: publicUser.name ?? "--", password: enteredPassword) + } label: { + L10n.signIn.text + } + } label: { + HStack { + ImageView(publicUser.profileImageSource(maxWidth: 50, maxHeight: 50)) { + Image(systemName: "person.circle") + .resizable() + .frame(width: 50, height: 50) + } + .frame(width: 50, height: 50) + .clipShape(Circle()) + + Text(publicUser.name ?? "--") + Spacer() + } + } + } + } +} diff --git a/Swiftfin/Views/UserSignInView.swift b/Swiftfin/Views/UserSignInView/UserSignInView.swift similarity index 84% rename from Swiftfin/Views/UserSignInView.swift rename to Swiftfin/Views/UserSignInView/UserSignInView.swift index 7e37dbde..b6211f49 100644 --- a/Swiftfin/Views/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView/UserSignInView.swift @@ -11,6 +11,8 @@ import SwiftUI struct UserSignInView: View { + @EnvironmentObject + private var router: UserSignInCoordinator.Router @ObservedObject var viewModel: UserSignInViewModel @State @@ -47,10 +49,18 @@ struct UserSignInView: View { L10n.signInToServer(viewModel.server.name).text } + if viewModel.quickConnectEnabled { + Button { + router.route(to: \.quickConnect) + } label: { + L10n.quickConnect.text + } + } + Section { if !viewModel.publicUsers.isEmpty { ForEach(viewModel.publicUsers, id: \.id) { user in - UserLoginCellView(viewModel: viewModel, user: user) + PublicUserSignInView(viewModel: viewModel, publicUser: user) .disabled(viewModel.isLoading) } } else { @@ -65,9 +75,11 @@ struct UserSignInView: View { } header: { HStack { L10n.publicUsers.text + Spacer() + Button { - viewModel.loadUsers() + viewModel.getPublicUsers() } label: { Image(systemName: "arrow.clockwise.circle.fill") } @@ -85,6 +97,8 @@ struct UserSignInView: View { } .navigationTitle(L10n.signIn) .navigationBarBackButtonHidden(viewModel.isLoading) - .onAppear(perform: viewModel.loadUsers) + .onDisappear { + viewModel.stopQuickConnectAuthCheck() + } } }