[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:
parent
a13f604be0
commit
f9ebebe6dd
|
@ -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<BasicNavigationViewCoordinator> {
|
||||
|
||||
// MARK: - User Profile View
|
||||
|
||||
func makeUserProfileSettings(viewModel: SettingsViewModel) -> NavigationViewCoordinator<UserProfileSettingsCoordinator> {
|
||||
NavigationViewCoordinator(
|
||||
BasicNavigationViewCoordinator {
|
||||
CustomizeViewsSettings()
|
||||
}
|
||||
UserProfileSettingsCoordinator(viewModel: viewModel)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Customize Settings View
|
||||
|
||||
func makeCustomizeViewsSettings() -> NavigationViewCoordinator<CustomizeSettingsCoordinator> {
|
||||
NavigationViewCoordinator(
|
||||
CustomizeSettingsCoordinator()
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Experimental Settings View
|
||||
|
||||
func makeExperimentalSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator(
|
||||
BasicNavigationViewCoordinator {
|
||||
|
@ -197,24 +210,32 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
|||
)
|
||||
}
|
||||
|
||||
// MARK: - Poster Indicator Settings View
|
||||
|
||||
func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
IndicatorSettingsView()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Server Settings View
|
||||
|
||||
func makeServerDetail(server: ServerState) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
EditServerView(server: server)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Video Player Settings View
|
||||
|
||||
func makeVideoPlayerSettings() -> NavigationViewCoordinator<VideoPlayerSettingsCoordinator> {
|
||||
NavigationViewCoordinator(
|
||||
VideoPlayerSettingsCoordinator()
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Playback Settings View
|
||||
|
||||
func makePlaybackQualitySettings() -> NavigationViewCoordinator<PlaybackQualitySettingsCoordinator> {
|
||||
NavigationViewCoordinator(
|
||||
PlaybackQualitySettingsCoordinator()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -102,7 +102,7 @@ extension SelectUserView {
|
|||
.buttonStyle(.borderless)
|
||||
.buttonBorderShape(.circle)
|
||||
.contextMenu {
|
||||
Button("Delete", role: .destructive) {
|
||||
Button(L10n.delete, role: .destructive) {
|
||||
onDelete()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<UserState> = []
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1497,6 +1502,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -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 = "<group>";
|
||||
};
|
||||
BDFF67AF2D2CA59A009A9A3A /* UserProfileSettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */,
|
||||
BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */,
|
||||
);
|
||||
path = UserProfileSettingsView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue