[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
|
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()
|
||||||
|
|
|
@ -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
|
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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 */; };
|
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 */,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue