[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 <jpkribs@outlook.com>
Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Daniel Chick 2025-01-09 16:48:58 -06:00 committed by GitHub
parent a13f604be0
commit f9ebebe6dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 534 additions and 14 deletions

View File

@ -79,6 +79,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
var videoPlayerSettings = makeVideoPlayerSettings var videoPlayerSettings = makeVideoPlayerSettings
@Route(.modal) @Route(.modal)
var playbackQualitySettings = makePlaybackQualitySettings var playbackQualitySettings = makePlaybackQualitySettings
@Route(.modal)
var userProfile = makeUserProfileSettings
#endif #endif
#if os(iOS) #if os(iOS)
@ -181,14 +183,25 @@ final class SettingsCoordinator: NavigationCoordinatable {
#endif #endif
#if os(tvOS) #if os(tvOS)
func makeCustomizeViewsSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
// MARK: - User Profile View
func makeUserProfileSettings(viewModel: SettingsViewModel) -> NavigationViewCoordinator<UserProfileSettingsCoordinator> {
NavigationViewCoordinator( NavigationViewCoordinator(
BasicNavigationViewCoordinator { UserProfileSettingsCoordinator(viewModel: viewModel)
CustomizeViewsSettings()
}
) )
} }
// MARK: - Customize Settings View
func makeCustomizeViewsSettings() -> NavigationViewCoordinator<CustomizeSettingsCoordinator> {
NavigationViewCoordinator(
CustomizeSettingsCoordinator()
)
}
// MARK: - Experimental Settings View
func makeExperimentalSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> { func makeExperimentalSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator( NavigationViewCoordinator(
BasicNavigationViewCoordinator { BasicNavigationViewCoordinator {
@ -197,24 +210,32 @@ final class SettingsCoordinator: NavigationCoordinatable {
) )
} }
// MARK: - Poster Indicator Settings View
func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> { func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator { NavigationViewCoordinator {
IndicatorSettingsView() IndicatorSettingsView()
} }
} }
// MARK: - Server Settings View
func makeServerDetail(server: ServerState) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> { func makeServerDetail(server: ServerState) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator { NavigationViewCoordinator {
EditServerView(server: server) EditServerView(server: server)
} }
} }
// MARK: - Video Player Settings View
func makeVideoPlayerSettings() -> NavigationViewCoordinator<VideoPlayerSettingsCoordinator> { func makeVideoPlayerSettings() -> NavigationViewCoordinator<VideoPlayerSettingsCoordinator> {
NavigationViewCoordinator( NavigationViewCoordinator(
VideoPlayerSettingsCoordinator() VideoPlayerSettingsCoordinator()
) )
} }
// MARK: - Playback Settings View
func makePlaybackQualitySettings() -> NavigationViewCoordinator<PlaybackQualitySettingsCoordinator> { func makePlaybackQualitySettings() -> NavigationViewCoordinator<PlaybackQualitySettingsCoordinator> {
NavigationViewCoordinator( NavigationViewCoordinator(
PlaybackQualitySettingsCoordinator() PlaybackQualitySettingsCoordinator()

View File

@ -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<BasicNavigationViewCoordinator> {
NavigationViewCoordinator(
BasicNavigationViewCoordinator {
UserLocalSecurityView()
}
)
}
@ViewBuilder
func makeStart() -> some View {
UserProfileSettingsView(viewModel: viewModel)
}
}

View File

@ -50,7 +50,7 @@ struct SeriesEpisodeSelector: View {
selection = viewModel.seasons.first?.id selection = viewModel.seasons.first?.id
} }
} }
.onChange(of: selection) { _ in .onChange(of: selection) { _, _ in
guard let selectionViewModel else { return } guard let selectionViewModel else { return }
if selectionViewModel.state == .initial { if selectionViewModel.state == .initial {

View File

@ -102,7 +102,7 @@ extension SelectUserView {
.buttonStyle(.borderless) .buttonStyle(.borderless)
.buttonBorderShape(.circle) .buttonBorderShape(.circle)
.contextMenu { .contextMenu {
Button("Delete", role: .destructive) { Button(L10n.delete, role: .destructive) {
onDelete() onDelete()
} }
} }

View File

@ -55,6 +55,8 @@ struct SelectUserView: View {
@State @State
private var padGridItemColumnCount: Int = 1 private var padGridItemColumnCount: Int = 1
@State @State
private var pin: String = ""
@State
private var scrollViewOffset: CGFloat = 0 private var scrollViewOffset: CGFloat = 0
@State @State
private var selectedUsers: Set<UserState> = [] private var selectedUsers: Set<UserState> = []
@ -78,6 +80,8 @@ struct SelectUserView: View {
private var isPresentingConfirmDeleteUsers = false private var isPresentingConfirmDeleteUsers = false
@State @State
private var isPresentingServers: Bool = false private var isPresentingServers: Bool = false
@State
private var isPresentingLocalPin: Bool = false
// MARK: - Error State // MARK: - Error State
@ -163,6 +167,28 @@ struct SelectUserView: View {
return CGFloat(lastRowMissing) * (gridItemSize.width + EdgeInsets.edgePadding) / 2 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 // MARK: - Grid Content View
@ViewBuilder @ViewBuilder
@ -200,7 +226,7 @@ struct SelectUserView: View {
if isEditingUsers { if isEditingUsers {
selectedUsers.toggle(value: user) selectedUsers.toggle(value: user)
} else { } else {
viewModel.send(.signIn(user, pin: "")) select(user: user)
} }
} onDelete: { } onDelete: {
selectedUsers.insert(user) selectedUsers.insert(user)
@ -364,6 +390,13 @@ struct SelectUserView: View {
allServersSelection: .all allServersSelection: .all
) )
} }
.onChange(of: isPresentingLocalPin) { _, newValue in
if newValue {
pin = ""
} else {
selectedUsers.removeAll()
}
}
.onChange(of: viewModel.servers) { _, _ in .onChange(of: viewModel.servers) { _, _ in
gridItems = makeGridItems(for: serverSelection) gridItems = makeGridItems(for: serverSelection)
@ -409,6 +442,32 @@ struct SelectUserView: View {
Text(L10n.deleteUserMultipleConfirmation(selectedUsers.count)) 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) .errorMessage($error)
} }
} }

View File

@ -33,11 +33,8 @@ struct SettingsView: View {
.contentView { .contentView {
Section(L10n.jellyfin) { Section(L10n.jellyfin) {
Button {} label: { UserProfileRow(user: viewModel.userSession.user.data) {
TextPairView( router.route(to: \.userProfile, viewModel)
leading: L10n.user,
trailing: viewModel.userSession.user.username
)
} }
ChevronButton( ChevronButton(

View File

@ -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<Bool>(
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()
}
}
}

View File

@ -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)
}
}
}

View File

@ -146,6 +146,7 @@
4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; }; 4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; };
4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; }; 4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; };
4E762AAF2C3A1A95004D1579 /* 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 */; }; 4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */; };
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; 4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; };
4E8B34EB2AB91B6E0018F305 /* 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 */; }; BD39577C2C113FAA0078CEF8 /* TimestampSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39577B2C113FAA0078CEF8 /* TimestampSection.swift */; };
BD39577E2C1140810078CEF8 /* TransitionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39577D2C1140810078CEF8 /* TransitionSection.swift */; }; BD39577E2C1140810078CEF8 /* TransitionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39577D2C1140810078CEF8 /* TransitionSection.swift */; };
BDA623532D0D0854009A157F /* SelectUserBottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA623522D0D0854009A157F /* SelectUserBottomBar.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 */; }; C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; };
C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */; }; C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */; };
C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.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 = "<group>"; }; 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = "<group>"; };
4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeUnusedStrings.swift; sourceTree = "<group>"; }; 4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeUnusedStrings.swift; sourceTree = "<group>"; };
4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = "<group>"; }; 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = "<group>"; };
4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsCoordinator.swift; sourceTree = "<group>"; };
4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreModal.swift; sourceTree = "<group>"; }; 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreModal.swift; sourceTree = "<group>"; };
4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; }; 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; };
4E8F74A02CE03C8B00CC8969 /* ItemEditorCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemEditorCoordinator.swift; sourceTree = "<group>"; }; 4E8F74A02CE03C8B00CC8969 /* ItemEditorCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemEditorCoordinator.swift; sourceTree = "<group>"; };
@ -1497,6 +1502,8 @@
BD39577B2C113FAA0078CEF8 /* TimestampSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampSection.swift; sourceTree = "<group>"; }; BD39577B2C113FAA0078CEF8 /* TimestampSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampSection.swift; sourceTree = "<group>"; };
BD39577D2C1140810078CEF8 /* TransitionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionSection.swift; sourceTree = "<group>"; }; BD39577D2C1140810078CEF8 /* TransitionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionSection.swift; sourceTree = "<group>"; };
BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectUserBottomBar.swift; sourceTree = "<group>"; }; BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectUserBottomBar.swift; sourceTree = "<group>"; };
BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalSecurityView.swift; sourceTree = "<group>"; };
BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsView.swift; sourceTree = "<group>"; };
C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTypeLibraryViewModel.swift; sourceTree = "<group>"; }; C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTypeLibraryViewModel.swift; sourceTree = "<group>"; };
C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveSmallPlaybackButton.swift; sourceTree = "<group>"; }; C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveSmallPlaybackButton.swift; sourceTree = "<group>"; };
C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLargePlaybackButtons.swift; sourceTree = "<group>"; }; C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLargePlaybackButtons.swift; sourceTree = "<group>"; };
@ -3454,6 +3461,7 @@
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
E13DD4012717EE79009D4DAF /* SelectUserCoordinator.swift */, E13DD4012717EE79009D4DAF /* SelectUserCoordinator.swift */,
6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */,
4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */,
E11C15342BF7C505006BC9B6 /* UserProfileImageCoordinator.swift */, E11C15342BF7C505006BC9B6 /* UserProfileImageCoordinator.swift */,
E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */, E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */,
E18A8E8428D60D0000333B9A /* VideoPlayerCoordinator.swift */, E18A8E8428D60D0000333B9A /* VideoPlayerCoordinator.swift */,
@ -3485,6 +3493,15 @@
path = Sections; path = Sections;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
BDFF67AF2D2CA59A009A9A3A /* UserProfileSettingsView */ = {
isa = PBXGroup;
children = (
BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */,
BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */,
);
path = UserProfileSettingsView;
sourceTree = "<group>";
};
C44FA6DD2AACD15300EDEB56 /* PlaybackButtons */ = { C44FA6DD2AACD15300EDEB56 /* PlaybackButtons */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -4532,6 +4549,7 @@
E1A1528928FD22F600600579 /* TextPairView.swift */, E1A1528928FD22F600600579 /* TextPairView.swift */,
E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */, E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */,
4E7315722D14752400EA2A95 /* UserProfileImage */, 4E7315722D14752400EA2A95 /* UserProfileImage */,
E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */,
E1B5784028F8AFCB00D42911 /* WrappedView.swift */, E1B5784028F8AFCB00D42911 /* WrappedView.swift */,
); );
path = Components; path = Components;
@ -4596,7 +4614,6 @@
E1BE1CEC2BDB68C4008176A9 /* Components */ = { E1BE1CEC2BDB68C4008176A9 /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@ -4796,6 +4813,7 @@
E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */, E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */,
4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */, 4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */,
5398514426B64DA100101B49 /* SettingsView.swift */, 5398514426B64DA100101B49 /* SettingsView.swift */,
BDFF67AF2D2CA59A009A9A3A /* UserProfileSettingsView */,
E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */, E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */,
); );
path = SettingsView; path = SettingsView;
@ -5401,6 +5419,7 @@
E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */, E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */,
E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */, E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */,
E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */, E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */,
4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */,
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */, E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */,
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */, E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */,
E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */, E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */,
@ -5517,6 +5536,8 @@
E19D41B42BF2C0020082B8B2 /* StoredValues+Temp.swift in Sources */, E19D41B42BF2C0020082B8B2 /* StoredValues+Temp.swift in Sources */,
4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */, 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */,
E11BDF7B2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */, E11BDF7B2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */,
BDFF67B02D2CA59A009A9A3A /* UserLocalSecurityView.swift in Sources */,
BDFF67B22D2CA59A009A9A3A /* UserProfileSettingsView.swift in Sources */,
4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */, 4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */,
E1575E8C293E7B1E001665B1 /* UIScreen.swift in Sources */, E1575E8C293E7B1E001665B1 /* UIScreen.swift in Sources */,
C46DD8EC2A8FB49A0046A504 /* LiveMainOverlay.swift in Sources */, C46DD8EC2A8FB49A0046A504 /* LiveMainOverlay.swift in Sources */,
@ -5532,6 +5553,7 @@
4E661A1B2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */, 4E661A1B2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */,
E129428628F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */, E129428628F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */,
53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */,
BDFF67B32D2CA99D009A9A3A /* UserProfileRow.swift in Sources */,
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
E149CCAE2BE6ECC8008B9331 /* Storable.swift in Sources */, E149CCAE2BE6ECC8008B9331 /* Storable.swift in Sources */,
C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */, C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */,

View File

@ -37,7 +37,7 @@ struct UserLocalSecurityView: View {
@State @State
private var listSize: CGSize = .zero private var listSize: CGSize = .zero
@State @State
private var onPinCompletion: (() -> Void)? = nil private var onPinCompletion: (() -> Void)?
@State @State
private var pin: String = "" private var pin: String = ""
@State @State