From f9ebebe6dd944656ee33a33e963884eb48e8a399 Mon Sep 17 00:00:00 2001 From: Daniel Chick Date: Thu, 9 Jan 2025 16:48:58 -0600 Subject: [PATCH] [tvOS] Add pin prompt to sign-in screen (#1383) * Add pin prompt to sign-in screen * Bring over security views from iOS * silence tvOS 17 warnings * Add user profile and security views to routing * Changes * revert and remove commented code * cleanup * CodeFactor fixes * Joe's Suggestions: - Move UserProfileSettings to their own Coordinator - Make Views Modal to better reflect existing items - Fix CustomizeSettingsCoordinator (This is on me!) - Change PINs to use SecureField - Move all Settings View to use SplitFormWindowView to mirror existing Settings - Use user profile image for SplitFormWindowView Icon - Change Profile Security to use LearnMoreModal - Use suggestion from https://forums.developer.apple.com/forums/thread/739545 - Tag Alert > TextFields with TODO so we can check this on tvOS 18 * Fix PIN for https://forums.developer.apple.com/forums/thread/739545 on SelectUserView * Fix Build Issue. * use user --------- Co-authored-by: chickdan <=> Co-authored-by: Joe Co-authored-by: Ethan Pippin --- .../Components/UserProfileRow.swift | 0 Shared/Coordinators/SettingsCoordinator.swift | 29 +- .../UserProfileSettingsCoordinator.swift | 51 ++++ .../EpisodeSelector/EpisodeSelector.swift | 2 +- .../Components/UserGridButton.swift | 2 +- .../Views/SelectUserView/SelectUserView.swift | 61 +++- .../Views/SettingsView/SettingsView.swift | 7 +- .../UserLocalSecurityView.swift | 278 ++++++++++++++++++ .../UserProfileSettingsView.swift | 92 ++++++ Swiftfin.xcodeproj/project.pbxproj | 24 +- .../UserLocalSecurityView.swift | 2 +- 11 files changed, 534 insertions(+), 14 deletions(-) rename {Swiftfin/Views/SettingsView/SettingsView => Shared}/Components/UserProfileRow.swift (100%) create mode 100644 Shared/Coordinators/UserProfileSettingsCoordinator.swift create mode 100644 Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift create mode 100644 Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift diff --git a/Swiftfin/Views/SettingsView/SettingsView/Components/UserProfileRow.swift b/Shared/Components/UserProfileRow.swift similarity index 100% rename from Swiftfin/Views/SettingsView/SettingsView/Components/UserProfileRow.swift rename to Shared/Components/UserProfileRow.swift diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index 00c566f0..56b5cd17 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -79,6 +79,8 @@ final class SettingsCoordinator: NavigationCoordinatable { var videoPlayerSettings = makeVideoPlayerSettings @Route(.modal) var playbackQualitySettings = makePlaybackQualitySettings + @Route(.modal) + var userProfile = makeUserProfileSettings #endif #if os(iOS) @@ -181,14 +183,25 @@ final class SettingsCoordinator: NavigationCoordinatable { #endif #if os(tvOS) - func makeCustomizeViewsSettings() -> NavigationViewCoordinator { + + // MARK: - User Profile View + + func makeUserProfileSettings(viewModel: SettingsViewModel) -> NavigationViewCoordinator { NavigationViewCoordinator( - BasicNavigationViewCoordinator { - CustomizeViewsSettings() - } + UserProfileSettingsCoordinator(viewModel: viewModel) ) } + // MARK: - Customize Settings View + + func makeCustomizeViewsSettings() -> NavigationViewCoordinator { + NavigationViewCoordinator( + CustomizeSettingsCoordinator() + ) + } + + // MARK: - Experimental Settings View + func makeExperimentalSettings() -> NavigationViewCoordinator { NavigationViewCoordinator( BasicNavigationViewCoordinator { @@ -197,24 +210,32 @@ final class SettingsCoordinator: NavigationCoordinatable { ) } + // MARK: - Poster Indicator Settings View + func makeIndicatorSettings() -> NavigationViewCoordinator { NavigationViewCoordinator { IndicatorSettingsView() } } + // MARK: - Server Settings View + func makeServerDetail(server: ServerState) -> NavigationViewCoordinator { NavigationViewCoordinator { EditServerView(server: server) } } + // MARK: - Video Player Settings View + func makeVideoPlayerSettings() -> NavigationViewCoordinator { NavigationViewCoordinator( VideoPlayerSettingsCoordinator() ) } + // MARK: - Playback Settings View + func makePlaybackQualitySettings() -> NavigationViewCoordinator { NavigationViewCoordinator( PlaybackQualitySettingsCoordinator() diff --git a/Shared/Coordinators/UserProfileSettingsCoordinator.swift b/Shared/Coordinators/UserProfileSettingsCoordinator.swift new file mode 100644 index 00000000..7fada9bf --- /dev/null +++ b/Shared/Coordinators/UserProfileSettingsCoordinator.swift @@ -0,0 +1,51 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Stinsen +import SwiftUI + +final class UserProfileSettingsCoordinator: NavigationCoordinatable { + + // MARK: - Navigation Components + + let stack = Stinsen.NavigationStack(initial: \UserProfileSettingsCoordinator.start) + + @Root + var start = makeStart + + // MARK: - Route to User Profile Security + + @Route(.modal) + var localSecurity = makeLocalSecurity + + // MARK: - Observed Object + + @ObservedObject + var viewModel: SettingsViewModel + + // MARK: - Initializer + + init(viewModel: SettingsViewModel) { + self.viewModel = viewModel + } + + // MARK: - User Security View + + func makeLocalSecurity() -> NavigationViewCoordinator { + NavigationViewCoordinator( + BasicNavigationViewCoordinator { + UserLocalSecurityView() + } + ) + } + + @ViewBuilder + func makeStart() -> some View { + UserProfileSettingsView(viewModel: viewModel) + } +} diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift index 5edee9ef..f027096b 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift @@ -50,7 +50,7 @@ struct SeriesEpisodeSelector: View { selection = viewModel.seasons.first?.id } } - .onChange(of: selection) { _ in + .onChange(of: selection) { _, _ in guard let selectionViewModel else { return } if selectionViewModel.state == .initial { diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift b/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift index 92c8ca2b..35911a54 100644 --- a/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift +++ b/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift @@ -102,7 +102,7 @@ extension SelectUserView { .buttonStyle(.borderless) .buttonBorderShape(.circle) .contextMenu { - Button("Delete", role: .destructive) { + Button(L10n.delete, role: .destructive) { onDelete() } } diff --git a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift index 92b0ceea..3290a90c 100644 --- a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift @@ -55,6 +55,8 @@ struct SelectUserView: View { @State private var padGridItemColumnCount: Int = 1 @State + private var pin: String = "" + @State private var scrollViewOffset: CGFloat = 0 @State private var selectedUsers: Set = [] @@ -78,6 +80,8 @@ struct SelectUserView: View { private var isPresentingConfirmDeleteUsers = false @State private var isPresentingServers: Bool = false + @State + private var isPresentingLocalPin: Bool = false // MARK: - Error State @@ -163,6 +167,28 @@ struct SelectUserView: View { return CGFloat(lastRowMissing) * (gridItemSize.width + EdgeInsets.edgePadding) / 2 } + // MARK: - Select User(s) + + private func select(user: UserState, needsPin: Bool = true) { + Task { @MainActor in + selectedUsers.insert(user) + + switch user.accessPolicy { + case .requireDeviceAuthentication: + // Do nothing, no device authentication on tvOS + break + case .requirePin: + if needsPin { + isPresentingLocalPin = true + return + } + case .none: () + } + + viewModel.send(.signIn(user, pin: pin)) + } + } + // MARK: - Grid Content View @ViewBuilder @@ -200,7 +226,7 @@ struct SelectUserView: View { if isEditingUsers { selectedUsers.toggle(value: user) } else { - viewModel.send(.signIn(user, pin: "")) + select(user: user) } } onDelete: { selectedUsers.insert(user) @@ -364,6 +390,13 @@ struct SelectUserView: View { allServersSelection: .all ) } + .onChange(of: isPresentingLocalPin) { _, newValue in + if newValue { + pin = "" + } else { + selectedUsers.removeAll() + } + } .onChange(of: viewModel.servers) { _, _ in gridItems = makeGridItems(for: serverSelection) @@ -409,6 +442,32 @@ struct SelectUserView: View { Text(L10n.deleteUserMultipleConfirmation(selectedUsers.count)) } } + .alert(L10n.signIn, isPresented: $isPresentingLocalPin) { + + // TODO: Verify on tvOS 18 + // https://forums.developer.apple.com/forums/thread/739545 + // TextField(L10n.pin, text: $pin) + TextField(text: $pin) {} + .keyboardType(.numberPad) + + Button(L10n.signIn) { + guard let user = selectedUsers.first else { + assertionFailure("User not selected") + return + } + select(user: user, needsPin: false) + } + + Button(L10n.cancel, role: .cancel) {} + } message: { + if let user = selectedUsers.first, user.pinHint.isNotEmpty { + Text(user.pinHint) + } else { + let username = selectedUsers.first?.username ?? .emptyDash + + Text(L10n.enterPinForUser(username)) + } + } .errorMessage($error) } } diff --git a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift index eacbd9a2..69be51e1 100644 --- a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift @@ -33,11 +33,8 @@ struct SettingsView: View { .contentView { Section(L10n.jellyfin) { - Button {} label: { - TextPairView( - leading: L10n.user, - trailing: viewModel.userSession.user.username - ) + UserProfileRow(user: viewModel.userSession.user.data) { + router.route(to: \.userProfile, viewModel) } ChevronButton( diff --git a/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift b/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift new file mode 100644 index 00000000..55c1f971 --- /dev/null +++ b/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift @@ -0,0 +1,278 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Combine +import Defaults +import KeychainSwift +import SwiftUI + +// TODO: present toast when authentication successfully changed +// TODO: pop is just a workaround to get change published from usersession. +// find fix and don't pop when successfully changed +// TODO: could cleanup/refactor greatly + +struct UserLocalSecurityView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - State & Environment Objects + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @StateObject + private var viewModel = UserLocalSecurityViewModel() + + // MARK: - Local Security Variables + + @State + private var listSize: CGSize = .zero + @State + private var onPinCompletion: (() -> Void)? + @State + private var pin: String = "" + @State + private var pinHint: String = "" + @State + private var signInPolicy: UserAccessPolicy = .none + + // MARK: - Dialog States + + @State + private var isPresentingOldPinPrompt: Bool = false + @State + private var isPresentingNewPinPrompt: Bool = false + + // MARK: - Error State + + @State + private var error: Error? = nil + + // MARK: - Focus Management + + @FocusState + private var focusedItem: FocusableItem? + + private enum FocusableItem: Hashable { + case security + } + + // MARK: - Check Old Policy + + private func checkOldPolicy() { + do { + try viewModel.checkForOldPolicy() + } catch { + return + } + + checkNewPolicy() + } + + // MARK: - Check New Policy + + private func checkNewPolicy() { + do { + try viewModel.checkFor(newPolicy: signInPolicy) + } catch { + return + } + + viewModel.set(newPolicy: signInPolicy, newPin: pin, newPinHint: pinHint) + } + + // MARK: - Event Handler + + private func onReceive(_ event: UserLocalSecurityViewModel.Event) { + switch event { + case let .error(eventError): + error = eventError + case .promptForOldPin: + onPinCompletion = { + Task { + try viewModel.check(oldPin: pin) + + checkNewPolicy() + } + } + + pin = "" + isPresentingOldPinPrompt = true + case .promptForNewPin: + onPinCompletion = { + viewModel.set(newPolicy: signInPolicy, newPin: pin, newPinHint: pinHint) + router.popLast() + } + + pin = "" + isPresentingNewPinPrompt = true + case .promptForOldDeviceAuth, .promptForNewDeviceAuth: + break + } + } + + // MARK: - Body + + var body: some View { + SplitFormWindowView() + .descriptionView { + descriptionView + } + .contentView { + Section { + Toggle( + L10n.pin, + isOn: Binding( + get: { signInPolicy == .requirePin }, + set: { signInPolicy = $0 ? .requirePin : .none } + ) + ) + .focused($focusedItem, equals: .security) + /* Picker(L10n.security, selection: $signInPolicy) { + ForEach(UserAccessPolicy.allCases.filter { $0 != .requireDeviceAuthentication }, id: \.self) { policy in + Text(policy.displayTitle) + } + } */ + } + + if signInPolicy == .requirePin { + Section { + ChevronAlertButton( + L10n.hint, + subtitle: pinHint, + description: L10n.setPinHintDescription + ) { + // TODO: Verify on tvOS 18 + // https://forums.developer.apple.com/forums/thread/739545 + // TextField(L10n.hint, text: $pinHint) + TextField(text: $pinHint) {} + } + } header: { + Text(L10n.hint) + } footer: { + Text(L10n.setPinHintDescription) + } + } + } + .animation(.linear, value: signInPolicy) + .navigationTitle(L10n.security) + .onFirstAppear { + pinHint = viewModel.userSession.user.pinHint + signInPolicy = viewModel.userSession.user.accessPolicy + } + .onReceive(viewModel.events) { event in + onReceive(event) + } + .topBarTrailing { + Button { + checkOldPolicy() + } label: { + Group { + if signInPolicy == .requirePin, signInPolicy == viewModel.userSession.user.accessPolicy { + Text(L10n.changePin) + } else { + Text(L10n.save) + } + } + .foregroundStyle(accentColor.overlayColor) + .font(.headline) + .padding(.vertical, 5) + .padding(.horizontal, 10) + .background { + accentColor + } + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + .trackingSize($listSize) + .alert( + L10n.enterPin, + isPresented: $isPresentingOldPinPrompt, + presenting: onPinCompletion + ) { completion in + + // TODO: Verify on tvOS 18 + // https://forums.developer.apple.com/forums/thread/739545 + // SecureField(L10n.pin, text: $pin) + SecureField(text: $pin) {} + .keyboardType(.numberPad) + + Button(L10n.continue) { + completion() + } + + Button(L10n.cancel, role: .cancel) {} + } message: { _ in + Text(L10n.enterPinForUser(viewModel.userSession.user.username)) + } + .alert( + L10n.setPin, + isPresented: $isPresentingNewPinPrompt, + presenting: onPinCompletion + ) { completion in + + // TODO: Verify on tvOS 18 + // https://forums.developer.apple.com/forums/thread/739545 + // SecureField(L10n.pin, text: $pin) + SecureField(text: $pin) {} + .keyboardType(.numberPad) + + Button(L10n.set) { + completion() + } + + Button(L10n.cancel, role: .cancel) {} + } message: { _ in + Text(L10n.createPinForUser(viewModel.userSession.user.username)) + } + .errorMessage($error) + } + + // MARK: - Description View Icon + + private var descriptionView: some View { + ZStack { + Image(systemName: "lock.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 400) + + focusedDescription + .transition(.opacity.animation(.linear(duration: 0.2))) + } + } + + // MARK: - Description View on Focus + + @ViewBuilder + private var focusedDescription: some View { + switch focusedItem { + case .security: + LearnMoreModal { + TextPair( + title: L10n.security, + subtitle: L10n.additionalSecurityAccessDescription + ) + TextPair( + title: UserAccessPolicy.requirePin.displayTitle, + subtitle: L10n.requirePinDescription + ) + TextPair( + title: UserAccessPolicy.none.displayTitle, + subtitle: L10n.saveUserWithoutAuthDescription + ) + } + + case nil: + EmptyView() + } + } +} diff --git a/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift new file mode 100644 index 00000000..043b3509 --- /dev/null +++ b/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift @@ -0,0 +1,92 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Factory +import JellyfinAPI +import SwiftUI + +struct UserProfileSettingsView: View { + + @EnvironmentObject + private var router: UserProfileSettingsCoordinator.Router + + @ObservedObject + private var viewModel: SettingsViewModel + @StateObject + private var profileImageViewModel: UserProfileImageViewModel + + @State + private var isPresentingConfirmReset: Bool = false + + init(viewModel: SettingsViewModel) { + self.viewModel = viewModel + self._profileImageViewModel = StateObject(wrappedValue: UserProfileImageViewModel(user: viewModel.userSession.user.data)) + } + + var body: some View { + SplitFormWindowView() + .descriptionView { + UserProfileImage( + userID: viewModel.userSession.user.id, + source: viewModel.userSession.user.profileImageSource( + client: viewModel.userSession.client, + maxWidth: 400 + ) + ) + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 400) + } + .contentView { + + // TODO: bring reset password to tvOS +// Section { +// ChevronButton(L10n.password) +// .onSelect { +// router.route(to: \.resetUserPassword, viewModel.userSession.user.id) +// } +// } + + Section { + ChevronButton(L10n.security) + .onSelect { + router.route(to: \.localSecurity) + } + } + + // TODO: Do we want this option on tvOS? +// Section { +// // TODO: move under future "Storage" tab +// // when downloads implemented +// Button(L10n.resetSettings) { +// isPresentingConfirmReset = true +// } +// .foregroundStyle(.red) +// } footer: { +// Text(L10n.resetSettingsDescription) +// } + } + .withDescriptionTopPadding() + .navigationTitle(L10n.user) + .confirmationDialog( + L10n.resetSettings, + isPresented: $isPresentingConfirmReset, + titleVisibility: .visible + ) { + Button(L10n.reset, role: .destructive) { + do { + try viewModel.userSession.user.deleteSettings() + } catch { + viewModel.logger.error("Unable to reset user settings: \(error.localizedDescription)") + } + } + } message: { + Text(L10n.resetSettingsMessage) + } + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 2d511894..de6413a7 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -146,6 +146,7 @@ 4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; }; 4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; }; 4E762AAF2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; }; + 4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */; }; 4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */; }; 4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; 4E8B34EB2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; @@ -379,6 +380,9 @@ BD39577C2C113FAA0078CEF8 /* TimestampSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39577B2C113FAA0078CEF8 /* TimestampSection.swift */; }; BD39577E2C1140810078CEF8 /* TransitionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39577D2C1140810078CEF8 /* TransitionSection.swift */; }; BDA623532D0D0854009A157F /* SelectUserBottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */; }; + BDFF67B02D2CA59A009A9A3A /* UserLocalSecurityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */; }; + BDFF67B22D2CA59A009A9A3A /* UserProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */; }; + BDFF67B32D2CA99D009A9A3A /* UserProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */; }; C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; }; C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */; }; C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */; }; @@ -1292,6 +1296,7 @@ 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = ""; }; 4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeUnusedStrings.swift; sourceTree = ""; }; 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = ""; }; + 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsCoordinator.swift; sourceTree = ""; }; 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreModal.swift; sourceTree = ""; }; 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = ""; }; 4E8F74A02CE03C8B00CC8969 /* ItemEditorCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemEditorCoordinator.swift; sourceTree = ""; }; @@ -1497,6 +1502,8 @@ BD39577B2C113FAA0078CEF8 /* TimestampSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampSection.swift; sourceTree = ""; }; BD39577D2C1140810078CEF8 /* TransitionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionSection.swift; sourceTree = ""; }; BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectUserBottomBar.swift; sourceTree = ""; }; + BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalSecurityView.swift; sourceTree = ""; }; + BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsView.swift; sourceTree = ""; }; C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTypeLibraryViewModel.swift; sourceTree = ""; }; C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveSmallPlaybackButton.swift; sourceTree = ""; }; C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLargePlaybackButtons.swift; sourceTree = ""; }; @@ -3454,6 +3461,7 @@ 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, E13DD4012717EE79009D4DAF /* SelectUserCoordinator.swift */, 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, + 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */, E11C15342BF7C505006BC9B6 /* UserProfileImageCoordinator.swift */, E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */, E18A8E8428D60D0000333B9A /* VideoPlayerCoordinator.swift */, @@ -3485,6 +3493,15 @@ path = Sections; sourceTree = ""; }; + BDFF67AF2D2CA59A009A9A3A /* UserProfileSettingsView */ = { + isa = PBXGroup; + children = ( + BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */, + BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */, + ); + path = UserProfileSettingsView; + sourceTree = ""; + }; C44FA6DD2AACD15300EDEB56 /* PlaybackButtons */ = { isa = PBXGroup; children = ( @@ -4532,6 +4549,7 @@ E1A1528928FD22F600600579 /* TextPairView.swift */, E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */, 4E7315722D14752400EA2A95 /* UserProfileImage */, + E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */, E1B5784028F8AFCB00D42911 /* WrappedView.swift */, ); path = Components; @@ -4596,7 +4614,6 @@ E1BE1CEC2BDB68C4008176A9 /* Components */ = { isa = PBXGroup; children = ( - E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */, ); path = Components; sourceTree = ""; @@ -4796,6 +4813,7 @@ E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */, 4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */, 5398514426B64DA100101B49 /* SettingsView.swift */, + BDFF67AF2D2CA59A009A9A3A /* UserProfileSettingsView */, E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */, ); path = SettingsView; @@ -5401,6 +5419,7 @@ E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */, E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */, E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */, + 4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */, E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */, E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */, E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */, @@ -5517,6 +5536,8 @@ E19D41B42BF2C0020082B8B2 /* StoredValues+Temp.swift in Sources */, 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */, E11BDF7B2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */, + BDFF67B02D2CA59A009A9A3A /* UserLocalSecurityView.swift in Sources */, + BDFF67B22D2CA59A009A9A3A /* UserProfileSettingsView.swift in Sources */, 4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */, E1575E8C293E7B1E001665B1 /* UIScreen.swift in Sources */, C46DD8EC2A8FB49A0046A504 /* LiveMainOverlay.swift in Sources */, @@ -5532,6 +5553,7 @@ 4E661A1B2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */, E129428628F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, + BDFF67B32D2CA99D009A9A3A /* UserProfileRow.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, E149CCAE2BE6ECC8008B9331 /* Storable.swift in Sources */, C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */, diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift index 703e8cf9..b341ab0f 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift @@ -37,7 +37,7 @@ struct UserLocalSecurityView: View { @State private var listSize: CGSize = .zero @State - private var onPinCompletion: (() -> Void)? = nil + private var onPinCompletion: (() -> Void)? @State private var pin: String = "" @State