diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index 5382cbc0..d1c8b037 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -29,6 +29,11 @@ final class SettingsCoordinator: NavigationCoordinatable { @Route(.push) var about = makeAbout + #if !os(tvOS) + @Route(.push) + var quickConnect = makeQuickConnectSettings + #endif + @ViewBuilder func makeServerDetail() -> some View { let viewModel = ServerDetailViewModel(server: SessionManager.main.currentLogin.server) @@ -60,6 +65,14 @@ final class SettingsCoordinator: NavigationCoordinatable { AboutView() } + #if !os(tvOS) + @ViewBuilder + func makeQuickConnectSettings() -> some View { + let viewModel = QuickConnectSettingsViewModel() + QuickConnectSettingsView(viewModel: viewModel) + } + #endif + @ViewBuilder func makeStart() -> some View { let viewModel = SettingsViewModel(server: SessionManager.main.currentLogin.server, user: SessionManager.main.currentLogin.user) diff --git a/Shared/Errors/NetworkError.swift b/Shared/Errors/NetworkError.swift index 7778f61a..f2e91809 100644 --- a/Shared/Errors/NetworkError.swift +++ b/Shared/Errors/NetworkError.swift @@ -91,7 +91,7 @@ enum NetworkError: Error { default: errorMessage = ErrorMessage(code: code, title: L10n.error, - message: L10n.unknownError) + message: displayMessage ?? L10n.unknownError) } } diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index 8b668a97..392492ec 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -34,6 +34,8 @@ internal enum L10n { internal static var audioAndCaptions: String { return L10n.tr("Localizable", "audioAndCaptions") } /// Audio Track internal static var audioTrack: String { return L10n.tr("Localizable", "audioTrack") } + /// Authorize + internal static var authorize: String { return L10n.tr("Localizable", "authorize") } /// Auto Play internal static var autoPlay: String { return L10n.tr("Localizable", "autoPlay") } /// Back @@ -262,6 +264,14 @@ internal enum L10n { internal static var programs: String { return L10n.tr("Localizable", "programs") } /// Public Users internal static var publicUsers: String { return L10n.tr("Localizable", "publicUsers") } + /// Quick Connect + internal static var quickConnect: String { return L10n.tr("Localizable", "quickConnect") } + /// Quick Connect code + internal static var quickConnectCode: String { return L10n.tr("Localizable", "quickConnectCode") } + /// Invalid Quick Connect code + internal static var quickConnectInvalidError: String { return L10n.tr("Localizable", "quickConnectInvalidError") } + /// Authorizing Quick Connect successful. Please continue on your other device. + internal static var quickConnectSuccessMessage: String { return L10n.tr("Localizable", "quickConnectSuccessMessage") } /// Rated internal static var rated: String { return L10n.tr("Localizable", "rated") } /// Recently Added diff --git a/Shared/ViewModels/QuickConnectSettingsViewModel.swift b/Shared/ViewModels/QuickConnectSettingsViewModel.swift new file mode 100644 index 00000000..eea1f7d2 --- /dev/null +++ b/Shared/ViewModels/QuickConnectSettingsViewModel.swift @@ -0,0 +1,46 @@ +// +// 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 + +final class QuickConnectSettingsViewModel: ViewModel { + + @Published + var quickConnectCode = "" + @Published + var showSuccessMessage = false + + var alertTitle: String { + var message: String = "" + if errorMessage?.code != ErrorMessage.noShowErrorCode { + message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n") + } + message.append(contentsOf: "\(errorMessage?.title ?? L10n.unknownError)") + return message + } + + func sendQuickConnect() { + QuickConnectAPI.authorize(code: self.quickConnectCode) + .trackActivity(loading) + .sink(receiveCompletion: { completion in + self.handleAPIRequestError(displayMessage: L10n.quickConnectInvalidError, completion: completion) + switch completion { + case .failure: + LogManager.log.debug("Invalid Quick Connect code entered") + default: + break + } + }, receiveValue: { _ in + // receiving a successful HTTP response indicates a valid code + LogManager.log.debug("Valid Quick connect code entered") + self.showSuccessMessage = true + }) + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index a88a23ca..6a9b535d 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -8,9 +8,11 @@ import Defaults import Foundation +import JellyfinAPI +import Stinsen import SwiftUI -final class SettingsViewModel: ObservableObject { +final class SettingsViewModel: ViewModel { var bitrates: [Bitrates] = [] var langs: [TrackLanguage] = [] diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 0b023128..32e037b3 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -17,9 +17,6 @@ final class UserSignInViewModel: ViewModel { var router: UserSignInCoordinator.Router? let server: SwiftfinStore.State.Server - @Published - var isLoadingUsers = false - @Published var publicUsers: [UserDto] = [] @@ -33,7 +30,7 @@ final class UserSignInViewModel: ViewModel { if errorMessage?.code != ErrorMessage.noShowErrorCode { message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n") } - message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")") + message.append(contentsOf: "\(errorMessage?.title ?? L10n.unknownError)") return message } @@ -58,13 +55,12 @@ final class UserSignInViewModel: ViewModel { } func loadUsers() { - self.isLoadingUsers = true UserAPI.getPublicUsers() + .trackActivity(loading) .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.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index a7f9eed1..e8fc468f 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -250,6 +250,8 @@ 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 */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */; }; @@ -746,6 +748,8 @@ 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 = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = ""; }; @@ -1060,6 +1064,7 @@ 09389CC626819B4500AE350E /* VideoPlayerModel.swift */, E126F73F278A655300A522BF /* VideoPlayerViewModel */, 625CB57B2678CE1000530A6E /* ViewModel.swift */, + 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -1884,6 +1889,7 @@ E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */, E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */, 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, + 6334175A287DDFB9000603CE /* QuickConnectSettingsView.swift */, ); path = SettingsView; sourceTree = ""; @@ -2476,12 +2482,14 @@ E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */, E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */, E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */, + 6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, + 6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */, 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, diff --git a/Swiftfin/Views/SettingsView/QuickConnectSettingsView.swift b/Swiftfin/Views/SettingsView/QuickConnectSettingsView.swift new file mode 100644 index 00000000..a6e47215 --- /dev/null +++ b/Swiftfin/Views/SettingsView/QuickConnectSettingsView.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 Foundation +import SwiftUI + +struct QuickConnectSettingsView: View { + + @ObservedObject + var viewModel: QuickConnectSettingsViewModel + + var body: some View { + Form { + Section(header: L10n.quickConnect.text) { + TextField(L10n.quickConnectCode, text: $viewModel.quickConnectCode) + .keyboardType(.numberPad) + .disabled(viewModel.isLoading) + + Button { + viewModel.sendQuickConnect() + } label: { + L10n.authorize.text + .font(.callout) + .disabled((viewModel.quickConnectCode.count != 6) || viewModel.isLoading) + } + } + .alert(isPresented: $viewModel.showSuccessMessage) { + Alert(title: L10n.quickConnect.text, + message: L10n.quickConnectSuccessMessage.text, + dismissButton: .default(L10n.ok.text)) + } + } + .alert(item: $viewModel.errorMessage) { _ in + Alert(title: Text(viewModel.alertTitle), + message: Text(viewModel.errorMessage?.message ?? L10n.unknownError), + dismissButton: .cancel()) + } + } +} diff --git a/Swiftfin/Views/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView.swift index b7395be5..9d03aeba 100644 --- a/Swiftfin/Views/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView.swift @@ -81,6 +81,17 @@ struct SettingsView: View { L10n.switchUser.text .font(.callout) } + + Button { + settingsRouter.route(to: \.quickConnect) + } label: { + HStack { + L10n.quickConnect.text + .foregroundColor(.primary) + Spacer() + Image(systemName: "chevron.right") + } + } } // TODO: Implement these for playback diff --git a/Swiftfin/Views/UserSignInView.swift b/Swiftfin/Views/UserSignInView.swift index 6c7dc759..9f86944a 100644 --- a/Swiftfin/Views/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView.swift @@ -71,7 +71,7 @@ struct UserSignInView: View { } label: { Image(systemName: "arrow.clockwise.circle.fill") } - .disabled(viewModel.isLoadingUsers || viewModel.isLoading) + .disabled(viewModel.isLoading) } } .headerProminence(.increased) diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 4471fd68..39a9f2cb 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ