Some More Cleanup, Reset User Settings (#1060)

This commit is contained in:
Ethan Pippin 2024-05-16 22:10:40 -06:00 committed by GitHub
parent 66c26553ad
commit 8d6167c00b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 116 additions and 93 deletions

View File

@ -65,7 +65,7 @@ struct CaseIterablePicker<Element: CaseIterable & Displayable & Hashable>: View
extension CaseIterablePicker {
init(title: String, selection: Binding<Element?>) {
init(_ title: String, selection: Binding<Element?>) {
self.init(
selection: selection,
label: { Text($0.displayTitle) },
@ -75,7 +75,7 @@ extension CaseIterablePicker {
)
}
init(title: String, selection: Binding<Element>) {
init(_ title: String, selection: Binding<Element>) {
let binding = Binding<Element?> {
selection.wrappedValue
} set: { newValue, _ in

View File

@ -140,9 +140,9 @@ extension Defaults.Keys {
enum Library {
static let cinematicBackground: Key<Bool> = UserKey("Customization.Library.cinematicBackground", default: true)
static let cinematicBackground: Key<Bool> = UserKey("libraryCinematicBackground", default: true)
static let enabledDrawerFilters: Key<[ItemFilterType]> = UserKey(
"Library.enabledDrawerFilters",
"libraryEnabledDrawerFilters",
default: ItemFilterType.allCases
)
static let displayType: Key<LibraryDisplayType> = UserKey("libraryViewType", default: .grid)
@ -158,7 +158,7 @@ extension Defaults.Keys {
enum Search {
static let enabledDrawerFilters: Key<[ItemFilterType]> = UserKey(
"Search.enabledDrawerFilters",
"searchEnabledDrawerFilters",
default: ItemFilterType.allCases
)
}

View File

@ -11,8 +11,6 @@ import CoreStore
import Foundation
import SwiftUI
// TODO: observation
/// A property wrapper for a stored `AnyData` object.
@propertyWrapper
struct StoredValue<Value: Codable>: DynamicProperty {
@ -116,6 +114,9 @@ extension StoredValue {
// Stored value doesn't exist but we want to observe it.
// Create default and get new publisher
// TODO: this still store unnecessary data if never changed,
// observe if changes were made and delete on deinit
do {
try AnyStoredData.store(
value: key.defaultValue(),

View File

@ -12,7 +12,7 @@ import JellyfinAPI
// Note: Temporary values to avoid refactoring or
// reduce complexity at local sites.
//
// Values can be cleaned up at any time so and are
// Values can be cleaned up at any time and are
// meant to have a short lifetime.
extension StoredValues.Keys {

View File

@ -92,7 +92,7 @@ extension StoredValues.Keys {
static func libraryDisplayType(parentID: String?) -> Key<LibraryDisplayType> {
CurrentUserKey(
parentID,
domain: "libraryDisplayType",
domain: "setting-libraryDisplayType",
default: Defaults[.Customization.Library.displayType]
)
}
@ -100,7 +100,7 @@ extension StoredValues.Keys {
static func libraryListColumnCount(parentID: String?) -> Key<Int> {
CurrentUserKey(
parentID,
domain: "libraryListColumnCount",
domain: "setting-libraryListColumnCount",
default: Defaults[.Customization.Library.listColumnCount]
)
}
@ -108,7 +108,7 @@ extension StoredValues.Keys {
static func libraryPosterType(parentID: String?) -> Key<PosterDisplayType> {
CurrentUserKey(
parentID,
domain: "libraryPosterType",
domain: "setting-libraryPosterType",
default: Defaults[.Customization.Library.posterType]
)
}
@ -119,7 +119,7 @@ extension StoredValues.Keys {
static func libraryFilters(parentID: String?) -> Key<ItemFilterCollection> {
CurrentUserKey(
parentID,
domain: "libraryFilters",
domain: "setting-libraryFilters",
default: ItemFilterCollection.default
)
}

View File

@ -115,7 +115,7 @@ extension UserState {
try SwiftfinStore.dataStack.perform { transaction in
let userData = try transaction.fetchAll(
From<AnyStoredData>()
.where(\.$ownerID == id)
.where(combineByAnd: Where(\.$ownerID == id), Where("%K BEGINSWITH %@", "domain", "setting"))
)
transaction.delete(userData)

View File

@ -44,7 +44,7 @@ struct ChevronButton: View {
extension ChevronButton {
init(title: String, subtitle: String? = nil) {
init(_ title: String, subtitle: String? = nil) {
self.init(
title: title,
subtitle: subtitle,

View File

@ -51,7 +51,7 @@ struct AppSettingsView: View {
// )
// }
//
// ChevronButton(title: "Logs")
// ChevronButton("Logs")
// .onSelect {
// router.route(to: \.log)
// }

View File

@ -62,7 +62,7 @@ struct CustomizeViewsSettings: View {
Section {
ChevronButton(title: "Indicators")
ChevronButton("Indicators")
.onSelect {
router.route(to: \.indicatorSettings)
}

View File

@ -41,7 +41,7 @@ struct SettingsView: View {
}
ChevronButton(
title: L10n.server,
L10n.server,
subtitle: viewModel.userSession.server.name
)
.onSelect {
@ -60,7 +60,7 @@ struct SettingsView: View {
InlineEnumToggle(title: "Video Player Type", selection: $videoPlayerType)
ChevronButton(title: L10n.videoPlayer)
ChevronButton(L10n.videoPlayer)
.onSelect {
router.route(to: \.videoPlayerSettings)
}
@ -70,12 +70,12 @@ struct SettingsView: View {
Section {
ChevronButton(title: L10n.customize)
ChevronButton(L10n.customize)
.onSelect {
router.route(to: \.customizeViewsSettings)
}
ChevronButton(title: L10n.experimental)
ChevronButton(L10n.experimental)
.onSelect {
router.route(to: \.experimentalSettings)
}
@ -86,7 +86,7 @@ struct SettingsView: View {
Section {
ChevronButton(title: "Logs")
ChevronButton("Logs")
.onSelect {
router.route(to: \.log)
}

View File

@ -44,7 +44,7 @@ struct VideoPlayerSettingsView: View {
Section {
ChevronButton(
title: "Resume Offset",
"Resume Offset",
subtitle: resumeOffset.secondLabel
)
.onSelect {
@ -55,7 +55,7 @@ struct VideoPlayerSettingsView: View {
}
Section {
ChevronButton(title: L10n.subtitleFont, subtitle: subtitleFontName)
ChevronButton(L10n.subtitleFont, subtitle: subtitleFontName)
.onSelect {
router.route(to: \.fontPicker, $subtitleFontName)
}

View File

@ -3773,13 +3773,11 @@
E18E021E2887492B0022598C /* RowDivider.swift in Sources */,
E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */,
E1D37F592B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift in Sources */,
53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */,
E1575E99293E7B1E001665B1 /* UIColor.swift in Sources */,
E1575E92293E7B1E001665B1 /* CGSize.swift in Sources */,
E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */,
E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */,
C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */,
C46DD8EA2A8FB45C0046A504 /* LiveOverlay.swift in Sources */,
E11E376D293E9CC1009EF240 /* VideoPlayerCoordinator.swift in Sources */,
@ -4307,7 +4305,6 @@
E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */,
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */,
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */,

View File

@ -1,5 +1,5 @@
{
"originHash" : "558c2e760c073dbad0a2bfbade5ccfa1b2962fdd8ab5f658d9bbfc4310623441",
"originHash" : "68a42015b2d42a14d418b6e13427443de55c970d5b3764bbc969e1b3f8c3a78b",
"pins" : [
{
"identity" : "blurhashkit",
@ -34,7 +34,7 @@
"location" : "https://github.com/LePips/CollectionVGrid",
"state" : {
"branch" : "main",
"revision" : "b50b5241df5fc1d71e5a09f6a87731c67c2a79e5"
"revision" : "7204e5f717ea571efb4600ecb71c2412e0dec921"
}
},
{
@ -114,8 +114,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke",
"state" : {
"revision" : "4625c73ea00a9fb4b4f3e28d95d0021a44af7e59",
"version" : "12.5.0"
"revision" : "8e431251dea0081b6ab154dab61a6ec74e4b6577",
"version" : "12.6.0"
}
},
{
@ -123,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Pulse",
"state" : {
"revision" : "d647e99f06abc94d63579e335ad4ce368195c149",
"version" : "4.0.5"
"revision" : "4f34c4f91cda623a7627e6d5e35dbbbb514b8daa",
"version" : "4.1.1"
}
},
{
@ -195,8 +195,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect",
"state" : {
"revision" : "0cd2a5a5895306bc21d54a2254302d24a9a571e4",
"version" : "1.1.3"
"revision" : "7dc5b287f8040e4ad5038739850b758e78f77808",
"version" : "1.1.4"
}
},
{

View File

@ -44,7 +44,7 @@ struct ChevronButton: View {
extension ChevronButton {
init(title: String, subtitle: String? = nil) {
init(_ title: String, subtitle: String? = nil) {
self.init(
title: title,
subtitle: subtitle,

View File

@ -39,7 +39,7 @@ struct AboutAppView: View {
trailing: "\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))"
)
ChevronButton(title: L10n.sourceCode)
ChevronButton(L10n.sourceCode)
.leadingView {
Image(.logoGithub)
.resizable()
@ -51,7 +51,7 @@ struct AboutAppView: View {
UIApplication.shared.open(.swiftfinGithub)
}
ChevronButton(title: L10n.bugsAndFeatures)
ChevronButton(L10n.bugsAndFeatures)
.leadingView {
Image(systemName: "plus.circle.fill")
.resizable()
@ -65,7 +65,7 @@ struct AboutAppView: View {
UIApplication.shared.open(.swiftfinGithubIssues)
}
ChevronButton(title: L10n.settings)
ChevronButton(L10n.settings)
.leadingView {
Image(systemName: "gearshape.fill")
.resizable()

View File

@ -37,21 +37,21 @@ struct AppSettingsView: View {
var body: some View {
Form {
ChevronButton(title: L10n.about)
ChevronButton(L10n.about)
.onSelect {
router.route(to: \.about, viewModel)
}
Section(L10n.accessibility) {
ChevronButton(title: L10n.appIcon)
ChevronButton(L10n.appIcon)
.onSelect {
router.route(to: \.appIconSelector, viewModel)
}
if !selectUserUseSplashscreen {
CaseIterablePicker(
title: L10n.appearance,
L10n.appearance,
selection: $appearance
)
}
@ -85,7 +85,7 @@ struct AppSettingsView: View {
SignOutIntervalSection()
ChevronButton(title: L10n.logs)
ChevronButton(L10n.logs)
.onSelect {
router.route(to: \.log)
}

View File

@ -24,7 +24,7 @@ struct MediaSourceInfoView: View {
{
Section(L10n.video) {
ForEach(videoStreams, id: \.self) { stream in
ChevronButton(title: stream.displayTitle ?? .emptyDash)
ChevronButton(stream.displayTitle ?? .emptyDash)
.onSelect {
router.route(to: \.mediaStreamInfo, stream)
}
@ -37,7 +37,7 @@ struct MediaSourceInfoView: View {
{
Section(L10n.audio) {
ForEach(audioStreams, id: \.self) { stream in
ChevronButton(title: stream.displayTitle ?? .emptyDash)
ChevronButton(stream.displayTitle ?? .emptyDash)
.onSelect {
router.route(to: \.mediaStreamInfo, stream)
}
@ -50,7 +50,7 @@ struct MediaSourceInfoView: View {
{
Section(L10n.subtitle) {
ForEach(subtitleStreams, id: \.self) { stream in
ChevronButton(title: stream.displayTitle ?? .emptyDash)
ChevronButton(stream.displayTitle ?? .emptyDash)
.onSelect {
router.route(to: \.mediaStreamInfo, stream)
}

View File

@ -68,7 +68,7 @@ struct CustomizeViewsSettings: View {
if UIDevice.isPhone {
Section {
CaseIterablePicker(title: L10n.items, selection: $itemViewType)
CaseIterablePicker(L10n.items, selection: $itemViewType)
}
if itemViewType == .cinematic {
@ -91,12 +91,12 @@ struct CustomizeViewsSettings: View {
Section {
ChevronButton(title: L10n.library)
ChevronButton(L10n.library)
.onSelect {
router.route(to: \.itemFilterDrawerSelector, $libraryEnabledDrawerFilters)
}
ChevronButton(title: L10n.search)
ChevronButton(L10n.search)
.onSelect {
router.route(to: \.itemFilterDrawerSelector, $searchEnabledDrawerFilters)
}
@ -114,28 +114,28 @@ struct CustomizeViewsSettings: View {
Section(L10n.posters) {
ChevronButton(title: L10n.indicators)
ChevronButton(L10n.indicators)
.onSelect {
router.route(to: \.indicatorSettings)
}
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
CaseIterablePicker(title: L10n.next, selection: $nextUpPosterType)
CaseIterablePicker(L10n.next, selection: $nextUpPosterType)
CaseIterablePicker(title: L10n.recentlyAdded, selection: $recentlyAddedPosterType)
CaseIterablePicker(L10n.recentlyAdded, selection: $recentlyAddedPosterType)
CaseIterablePicker(title: L10n.latestWithString(L10n.library), selection: $latestInLibraryPosterType)
CaseIterablePicker(L10n.latestWithString(L10n.library), selection: $latestInLibraryPosterType)
CaseIterablePicker(title: L10n.recommended, selection: $similarPosterType)
CaseIterablePicker(L10n.recommended, selection: $similarPosterType)
CaseIterablePicker(title: L10n.search, selection: $searchPosterType)
CaseIterablePicker(L10n.search, selection: $searchPosterType)
}
Section("Libraries") {
CaseIterablePicker(title: L10n.library, selection: $libraryDisplayType)
CaseIterablePicker(L10n.library, selection: $libraryDisplayType)
CaseIterablePicker(title: L10n.posters, selection: $libraryPosterType)
CaseIterablePicker(L10n.posters, selection: $libraryPosterType)
if libraryDisplayType == .list, UIDevice.isPad {
BasicStepper(

View File

@ -38,23 +38,23 @@ struct GestureSettingsView: View {
Section {
CaseIterablePicker(title: "Horizontal Pan", selection: $horizontalPanGesture)
CaseIterablePicker("Horizontal Pan", selection: $horizontalPanGesture)
.disabled(horizontalSwipeGesture != .none && horizontalPanGesture == .none)
CaseIterablePicker(title: "Horizontal Swipe", selection: $horizontalSwipeGesture)
CaseIterablePicker("Horizontal Swipe", selection: $horizontalSwipeGesture)
.disabled(horizontalPanGesture != .none && horizontalSwipeGesture == .none)
CaseIterablePicker(title: "Long Press", selection: $longPressGesture)
CaseIterablePicker("Long Press", selection: $longPressGesture)
CaseIterablePicker(title: "Multi Tap", selection: $multiTapGesture)
CaseIterablePicker("Multi Tap", selection: $multiTapGesture)
CaseIterablePicker(title: "Double Touch", selection: $doubleTouchGesture)
CaseIterablePicker("Double Touch", selection: $doubleTouchGesture)
CaseIterablePicker(title: "Pinch", selection: $pinchGesture)
CaseIterablePicker("Pinch", selection: $pinchGesture)
CaseIterablePicker(title: "Left Vertical Pan", selection: $verticalPanGestureLeft)
CaseIterablePicker("Left Vertical Pan", selection: $verticalPanGestureLeft)
CaseIterablePicker(title: "Right Vertical Pan", selection: $verticalPanGestureRight)
CaseIterablePicker("Right Vertical Pan", selection: $verticalPanGestureRight)
}
}
.navigationTitle("Gestures")

View File

@ -39,7 +39,7 @@ struct SettingsView: View {
// TODO: admin users go to dashboard instead
ChevronButton(
title: L10n.server,
L10n.server,
subtitle: viewModel.userSession.server.name
)
.onSelect {
@ -58,30 +58,30 @@ struct SettingsView: View {
Section(L10n.videoPlayer) {
CaseIterablePicker(
title: L10n.videoPlayerType,
L10n.videoPlayerType,
selection: $videoPlayerType
)
ChevronButton(title: L10n.nativePlayer)
ChevronButton(L10n.nativePlayer)
.onSelect {
router.route(to: \.nativePlayerSettings)
}
ChevronButton(title: L10n.videoPlayer)
ChevronButton(L10n.videoPlayer)
.onSelect {
router.route(to: \.videoPlayerSettings)
}
}
Section(L10n.accessibility) {
CaseIterablePicker(title: L10n.appearance, selection: $appearance)
CaseIterablePicker(L10n.appearance, selection: $appearance)
ChevronButton(title: L10n.customize)
ChevronButton(L10n.customize)
.onSelect {
router.route(to: \.customizeViewsSettings)
}
ChevronButton(title: L10n.experimental)
ChevronButton(L10n.experimental)
.onSelect {
router.route(to: \.experimentalSettings)
}
@ -93,14 +93,14 @@ struct SettingsView: View {
Text(L10n.accentColorDescription)
}
ChevronButton(title: L10n.logs)
ChevronButton(L10n.logs)
.onSelect {
router.route(to: \.log)
}
#if DEBUG
ChevronButton(title: "Debug")
ChevronButton("Debug")
.onSelect {
router.route(to: \.debugSettings)
}

View File

@ -91,7 +91,7 @@ struct ResetUserPasswordView: View {
}
.foregroundStyle(.red, .red.opacity(0.2))
} else {
ListRowButton("Reset") {
ListRowButton("Save") {
focusedPassword = nil
viewModel.send(.reset(current: currentPassword, new: confirmNewPassword))
}

View File

@ -106,7 +106,7 @@ struct UserLocalSecurityView: View {
List {
Section {
CaseIterablePicker(title: "Security", selection: $signInPolicy)
CaseIterablePicker("Security", selection: $signInPolicy)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(

View File

@ -17,6 +17,9 @@ struct UserProfileSettingsView: View {
@ObservedObject
var viewModel: SettingsViewModel
@State
private var isPresentingConfirmReset: Bool = false
@ViewBuilder
private var imageView: some View {
ImageView(
@ -63,23 +66,45 @@ struct UserProfileSettingsView: View {
}
Section {
ChevronButton(title: L10n.quickConnect)
ChevronButton(L10n.quickConnect)
.onSelect {
router.route(to: \.quickConnect)
}
ChevronButton(title: "Password")
ChevronButton("Password")
.onSelect {
router.route(to: \.resetUserPassword)
}
}
Section {
ChevronButton(title: "Local Security")
ChevronButton("Security")
.onSelect {
router.route(to: \.localSecurity)
}
}
Section {
// TODO: move under future "Storage" tab
// when downloads implemented
Button("Reset Settings") {
isPresentingConfirmReset = true
}
.foregroundStyle(.red)
} footer: {
Text("Reset Swiftfin user settings")
}
}
.alert("Reset Settings", isPresented: $isPresentingConfirmReset) {
Button("Reset", role: .destructive) {
do {
try viewModel.userSession.user.deleteSettings()
} catch {
viewModel.logger.error("Unable to reset user settings: \(error.localizedDescription)")
}
}
} message: {
Text("Are you sure you want to reset all user settings?")
}
}
}

View File

@ -65,14 +65,14 @@ struct VideoPlayerSettingsView: View {
var body: some View {
Form {
ChevronButton(title: L10n.gestures)
ChevronButton(L10n.gestures)
.onSelect {
router.route(to: \.gestureSettings)
}
CaseIterablePicker(title: L10n.jumpBackwardLength, selection: $jumpBackwardLength)
CaseIterablePicker(L10n.jumpBackwardLength, selection: $jumpBackwardLength)
CaseIterablePicker(title: L10n.jumpForwardLength, selection: $jumpForwardLength)
CaseIterablePicker(L10n.jumpForwardLength, selection: $jumpForwardLength)
Section {
@ -91,7 +91,7 @@ struct VideoPlayerSettingsView: View {
Section(L10n.buttons) {
CaseIterablePicker(title: L10n.playbackButtons, selection: $playbackButtonType)
CaseIterablePicker(L10n.playbackButtons, selection: $playbackButtonType)
Toggle(isOn: $showJumpButtons) {
HStack {
@ -100,12 +100,12 @@ struct VideoPlayerSettingsView: View {
}
}
ChevronButton(title: L10n.barButtons)
ChevronButton(L10n.barButtons)
.onSelect {
router.route(to: \.actionButtonSelector, $barActionButtons)
}
ChevronButton(title: L10n.menuButtons)
ChevronButton(L10n.menuButtons)
.onSelect {
router.route(to: \.actionButtonSelector, $menuActionButtons)
}
@ -119,12 +119,12 @@ struct VideoPlayerSettingsView: View {
Text(L10n.sliderColor)
}
CaseIterablePicker(title: L10n.sliderType, selection: $sliderType)
CaseIterablePicker(L10n.sliderType, selection: $sliderType)
}
Section {
ChevronButton(title: L10n.subtitleFont, subtitle: subtitleFontName)
ChevronButton(L10n.subtitleFont, subtitle: subtitleFontName)
.onSelect {
router.route(to: \.fontPicker, $subtitleFontName)
}
@ -150,9 +150,9 @@ struct VideoPlayerSettingsView: View {
Toggle(L10n.scrubCurrentTime, isOn: $showCurrentTimeWhileScrubbing)
CaseIterablePicker(title: L10n.timestampType, selection: $timestampType)
CaseIterablePicker(L10n.timestampType, selection: $timestampType)
CaseIterablePicker(title: L10n.trailingValue, selection: $trailingTimestampType)
CaseIterablePicker(L10n.trailingValue, selection: $trailingTimestampType)
}
Section(L10n.transition) {

View File

@ -43,7 +43,7 @@ extension UserSignInView {
List {
Section {
CaseIterablePicker(title: "Security", selection: $updateSignInPolicy)
CaseIterablePicker("Security", selection: $updateSignInPolicy)
} footer: {
// TODO: descriptions of each section

View File

@ -34,7 +34,7 @@ struct PlaybackSettingsView: View {
Form {
Section {
ChevronButton(title: L10n.videoPlayer)
ChevronButton(L10n.videoPlayer)
.onSelect {
router.route(to: \.videoPlayerSettings)
}
@ -67,7 +67,7 @@ struct PlaybackSettingsView: View {
if viewModel.videoStreams.isNotEmpty {
Section(L10n.video) {
ForEach(viewModel.videoStreams, id: \.displayTitle) { mediaStream in
ChevronButton(title: mediaStream.displayTitle ?? .emptyDash)
ChevronButton(mediaStream.displayTitle ?? .emptyDash)
.onSelect {
router.route(to: \.mediaStreamInfo, mediaStream)
}
@ -78,7 +78,7 @@ struct PlaybackSettingsView: View {
if viewModel.audioStreams.isNotEmpty {
Section(L10n.audio) {
ForEach(viewModel.audioStreams, id: \.displayTitle) { mediaStream in
ChevronButton(title: mediaStream.displayTitle ?? .emptyDash)
ChevronButton(mediaStream.displayTitle ?? .emptyDash)
.onSelect {
router.route(to: \.mediaStreamInfo, mediaStream)
}
@ -89,7 +89,7 @@ struct PlaybackSettingsView: View {
if viewModel.subtitleStreams.isNotEmpty {
Section(L10n.subtitle) {
ForEach(viewModel.subtitleStreams, id: \.displayTitle) { mediaStream in
ChevronButton(title: mediaStream.displayTitle ?? .emptyDash)
ChevronButton(mediaStream.displayTitle ?? .emptyDash)
.onSelect {
router.route(to: \.mediaStreamInfo, mediaStream)
}