Skip to content

Commit

Permalink
[tvOS] Add pin prompt to sign-in screen (jellyfin#1383)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Ethan Pippin <[email protected]>
  • Loading branch information
3 people authored Jan 9, 2025
1 parent a13f604 commit f9ebebe
Show file tree
Hide file tree
Showing 11 changed files with 534 additions and 14 deletions.
File renamed without changes.
29 changes: 25 additions & 4 deletions Shared/Coordinators/SettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
Expand Down
51 changes: 51 additions & 0 deletions Shared/Coordinators/UserProfileSettingsCoordinator.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ extension SelectUserView {
.buttonStyle(.borderless)
.buttonBorderShape(.circle)
.contextMenu {
Button("Delete", role: .destructive) {
Button(L10n.delete, role: .destructive) {
onDelete()
}
}
Expand Down
61 changes: 60 additions & 1 deletion Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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> = []
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}
}
7 changes: 2 additions & 5 deletions Swiftfin tvOS/Views/SettingsView/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading

0 comments on commit f9ebebe

Please sign in to comment.