[tvOS] Mirror iOS Ratings + Attribute Settings (#1422)

* Copy + Paste + Settings

* Much bigger changes to allow attribute customization.

* wip

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-02-15 15:27:34 -07:00 committed by GitHub
parent 846aabc868
commit c113c341bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 271 additions and 141 deletions

View File

@ -19,6 +19,8 @@ final class CustomizeSettingsCoordinator: NavigationCoordinatable {
@Route(.modal) @Route(.modal)
var indicatorSettings = makeIndicatorSettings var indicatorSettings = makeIndicatorSettings
@Route(.modal) @Route(.modal)
var itemViewAttributes = makeItemViewAttributes
@Route(.push)
var listColumnSettings = makeListColumnSettings var listColumnSettings = makeListColumnSettings
func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> { func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
@ -27,6 +29,15 @@ final class CustomizeSettingsCoordinator: NavigationCoordinatable {
} }
} }
func makeItemViewAttributes(selection: Binding<[ItemViewAttribute]>) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
OrderedSectionSelectorView(selection: selection, sources: ItemViewAttribute.allCases)
.systemImage("list.bullet.rectangle.fill")
.navigationTitle(L10n.mediaAttributes.localizedCapitalized)
}
}
@ViewBuilder
func makeListColumnSettings(selection: Binding<Int>) -> some View { func makeListColumnSettings(selection: Binding<Int>) -> some View {
ListColumnsPickerView(selection: selection) ListColumnsPickerView(selection: selection)
} }

View File

@ -44,6 +44,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
@Route(.push) @Route(.push)
var indicatorSettings = makeIndicatorSettings var indicatorSettings = makeIndicatorSettings
@Route(.push) @Route(.push)
var itemViewAttributes = makeItemViewAttributes
@Route(.push)
var serverConnection = makeServerConnection var serverConnection = makeServerConnection
@Route(.push) @Route(.push)
var videoPlayerSettings = makeVideoPlayerSettings var videoPlayerSettings = makeVideoPlayerSettings
@ -149,6 +151,12 @@ final class SettingsCoordinator: NavigationCoordinatable {
IndicatorSettingsView() IndicatorSettingsView()
} }
@ViewBuilder
func makeItemViewAttributes(selection: Binding<[ItemViewAttribute]>) -> some View {
OrderedSectionSelectorView(selection: selection, sources: ItemViewAttribute.allCases)
.navigationTitle(L10n.mediaAttributes.localizedCapitalized)
}
@ViewBuilder @ViewBuilder
func makeServerConnection(server: ServerState) -> some View { func makeServerConnection(server: ServerState) -> some View {
EditServerView(server: server) EditServerView(server: server)

View File

@ -0,0 +1,34 @@
//
// 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
//
enum ItemViewAttribute: String, CaseIterable, Displayable, Storable {
case ratingCritics
case ratingCommunity
case ratingOfficial
case videoQuality
case audioChannels
case subtitles
var displayTitle: String {
switch self {
case .ratingCritics:
return L10n.criticRating
case .ratingCommunity:
return L10n.communityRating
case .ratingOfficial:
return L10n.parentalRating
case .videoQuality:
return L10n.video
case .audioChannels:
return L10n.audio
case .subtitles:
return L10n.subtitles
}
}
}

View File

@ -280,6 +280,8 @@ internal enum L10n {
internal static let columns = L10n.tr("Localizable", "columns", fallback: "Columns") internal static let columns = L10n.tr("Localizable", "columns", fallback: "Columns")
/// Community /// Community
internal static let community = L10n.tr("Localizable", "community", fallback: "Community") internal static let community = L10n.tr("Localizable", "community", fallback: "Community")
/// Community rating
internal static let communityRating = L10n.tr("Localizable", "communityRating", fallback: "Community rating")
/// Compact /// Compact
internal static let compact = L10n.tr("Localizable", "compact", fallback: "Compact") internal static let compact = L10n.tr("Localizable", "compact", fallback: "Compact")
/// Compact Logo /// Compact Logo
@ -338,12 +340,14 @@ internal enum L10n {
} }
/// Creator /// Creator
internal static let creator = L10n.tr("Localizable", "creator", fallback: "Creator") internal static let creator = L10n.tr("Localizable", "creator", fallback: "Creator")
/// Critic rating
internal static let criticRating = L10n.tr("Localizable", "criticRating", fallback: "Critic rating")
/// Critics /// Critics
internal static let critics = L10n.tr("Localizable", "critics", fallback: "Critics") internal static let critics = L10n.tr("Localizable", "critics", fallback: "Critics")
/// Current /// Current
internal static let current = L10n.tr("Localizable", "current", fallback: "Current") internal static let current = L10n.tr("Localizable", "current", fallback: "Current")
/// Current Password /// Current password
internal static let currentPassword = L10n.tr("Localizable", "currentPassword", fallback: "Current Password") internal static let currentPassword = L10n.tr("Localizable", "currentPassword", fallback: "Current password")
/// Custom /// Custom
internal static let custom = L10n.tr("Localizable", "custom", fallback: "Custom") internal static let custom = L10n.tr("Localizable", "custom", fallback: "Custom")
/// Custom bitrate /// Custom bitrate
@ -368,10 +372,10 @@ internal enum L10n {
internal static let customFailedLogins = L10n.tr("Localizable", "customFailedLogins", fallback: "Custom failed logins") internal static let customFailedLogins = L10n.tr("Localizable", "customFailedLogins", fallback: "Custom failed logins")
/// Customize /// Customize
internal static let customize = L10n.tr("Localizable", "customize", fallback: "Customize") internal static let customize = L10n.tr("Localizable", "customize", fallback: "Customize")
/// Custom Profile /// Custom profile
internal static let customProfile = L10n.tr("Localizable", "customProfile", fallback: "Custom Profile") internal static let customProfile = L10n.tr("Localizable", "customProfile", fallback: "Custom profile")
/// Custom Rating /// Custom rating
internal static let customRating = L10n.tr("Localizable", "customRating", fallback: "Custom Rating") internal static let customRating = L10n.tr("Localizable", "customRating", fallback: "Custom rating")
/// Custom sessions /// Custom sessions
internal static let customSessions = L10n.tr("Localizable", "customSessions", fallback: "Custom sessions") internal static let customSessions = L10n.tr("Localizable", "customSessions", fallback: "Custom sessions")
/// Daily /// Daily
@ -384,10 +388,10 @@ internal enum L10n {
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.") internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.")
/// Date Added /// Date Added
internal static let dateAdded = L10n.tr("Localizable", "dateAdded", fallback: "Date Added") internal static let dateAdded = L10n.tr("Localizable", "dateAdded", fallback: "Date Added")
/// Date Created /// Date created
internal static let dateCreated = L10n.tr("Localizable", "dateCreated", fallback: "Date Created") internal static let dateCreated = L10n.tr("Localizable", "dateCreated", fallback: "Date created")
/// Date Modified /// Date modified
internal static let dateModified = L10n.tr("Localizable", "dateModified", fallback: "Date Modified") internal static let dateModified = L10n.tr("Localizable", "dateModified", fallback: "Date modified")
/// Date of death /// Date of death
internal static let dateOfDeath = L10n.tr("Localizable", "dateOfDeath", fallback: "Date of death") internal static let dateOfDeath = L10n.tr("Localizable", "dateOfDeath", fallback: "Date of death")
/// Dates /// Dates
@ -788,6 +792,8 @@ internal enum L10n {
internal static let media = L10n.tr("Localizable", "media", fallback: "Media") internal static let media = L10n.tr("Localizable", "media", fallback: "Media")
/// Media Access /// Media Access
internal static let mediaAccess = L10n.tr("Localizable", "mediaAccess", fallback: "Media Access") internal static let mediaAccess = L10n.tr("Localizable", "mediaAccess", fallback: "Media Access")
/// Media attributes
internal static let mediaAttributes = L10n.tr("Localizable", "mediaAttributes", fallback: "Media attributes")
/// Media downloads /// Media downloads
internal static let mediaDownloads = L10n.tr("Localizable", "mediaDownloads", fallback: "Media downloads") internal static let mediaDownloads = L10n.tr("Localizable", "mediaDownloads", fallback: "Media downloads")
/// Media playback /// Media playback
@ -872,8 +878,8 @@ internal enum L10n {
} }
/// No title /// No title
internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title") internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title")
/// Official Rating /// Official rating
internal static let officialRating = L10n.tr("Localizable", "officialRating", fallback: "Official Rating") internal static let officialRating = L10n.tr("Localizable", "officialRating", fallback: "Official rating")
/// Offset /// Offset
internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset") internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset")
/// OK /// OK
@ -902,8 +908,8 @@ internal enum L10n {
internal static let overview = L10n.tr("Localizable", "overview", fallback: "Overview") internal static let overview = L10n.tr("Localizable", "overview", fallback: "Overview")
/// Parental controls /// Parental controls
internal static let parentalControls = L10n.tr("Localizable", "parentalControls", fallback: "Parental controls") internal static let parentalControls = L10n.tr("Localizable", "parentalControls", fallback: "Parental controls")
/// Parental Rating /// Parental rating
internal static let parentalRating = L10n.tr("Localizable", "parentalRating", fallback: "Parental Rating") internal static let parentalRating = L10n.tr("Localizable", "parentalRating", fallback: "Parental rating")
/// Password /// Password
internal static let password = L10n.tr("Localizable", "password", fallback: "Password") internal static let password = L10n.tr("Localizable", "password", fallback: "Password")
/// User password has been changed. /// User password has been changed.
@ -994,8 +1000,8 @@ internal enum L10n {
internal static let quickConnectSuccessMessage = L10n.tr("Localizable", "quickConnectSuccessMessage", fallback: "Authorizing Quick Connect successful. Please continue on your other device.") internal static let quickConnectSuccessMessage = L10n.tr("Localizable", "quickConnectSuccessMessage", fallback: "Authorizing Quick Connect successful. Please continue on your other device.")
/// Random /// Random
internal static let random = L10n.tr("Localizable", "random", fallback: "Random") internal static let random = L10n.tr("Localizable", "random", fallback: "Random")
/// Random Image /// Random image
internal static let randomImage = L10n.tr("Localizable", "randomImage", fallback: "Random Image") internal static let randomImage = L10n.tr("Localizable", "randomImage", fallback: "Random image")
/// Rating /// Rating
internal static let rating = L10n.tr("Localizable", "rating", fallback: "Rating") internal static let rating = L10n.tr("Localizable", "rating", fallback: "Rating")
/// %@ rating on a scale from 1 to 10. /// %@ rating on a scale from 1 to 10.

View File

@ -172,5 +172,13 @@ extension StoredValues.Keys {
default: false default: false
) )
} }
static var itemViewAttributes: Key<[ItemViewAttribute]> {
CurrentUserKey(
"itemViewAttributes",
domain: "itemViewAttributes",
default: ItemViewAttribute.allCases
)
}
} }
} }

View File

@ -9,49 +9,81 @@
import SwiftUI import SwiftUI
extension ItemView { extension ItemView {
struct AttributesHStack: View { struct AttributesHStack: View {
@ObservedObject @ObservedObject
var viewModel: ItemViewModel var viewModel: ItemViewModel
@StoredValue(.User.itemViewAttributes)
private var itemViewAttributes
var body: some View { var body: some View {
HStack(spacing: 25) { HStack(spacing: 25) {
ForEach(itemViewAttributes, id: \.self) { attribute in
getAttribute(attribute)
}
}
.foregroundStyle(Color(UIColor.darkGray))
}
@ViewBuilder
func getAttribute(_ attribute: ItemViewAttribute) -> some View {
switch attribute {
case .ratingCritics:
if let criticRating = viewModel.item.criticRating {
HStack(spacing: 2) {
Group {
if criticRating >= 60 {
Image(.tomatoFresh)
.symbolRenderingMode(.hierarchical)
} else {
Image(.tomatoRotten)
}
}
.font(.caption2)
Text("\(criticRating, specifier: "%.0f")")
}
.asAttributeStyle(.outline)
}
case .ratingCommunity:
if let communityRating = viewModel.item.communityRating {
HStack(spacing: 2) {
Image(systemName: "star.fill")
.font(.caption2)
Text("\(communityRating, specifier: "%.1f")")
}
.asAttributeStyle(.outline)
}
case .ratingOfficial:
if let officialRating = viewModel.item.officialRating { if let officialRating = viewModel.item.officialRating {
Text(officialRating) Text(officialRating)
.asAttributeStyle(.outline) .asAttributeStyle(.outline)
} }
case .videoQuality:
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams { if viewModel.selectedMediaSource?.mediaStreams?.hasHDVideo == true {
Text("HD")
if mediaStreams.hasHDVideo { .asAttributeStyle(.fill)
Text("HD") }
.asAttributeStyle(.fill) if viewModel.selectedMediaSource?.mediaStreams?.has4KVideo == true {
} Text("4K")
.asAttributeStyle(.fill)
if mediaStreams.has4KVideo { }
Text("4K") case .audioChannels:
.asAttributeStyle(.fill) if viewModel.selectedMediaSource?.mediaStreams?.has51AudioChannelLayout == true {
} Text("5.1")
.asAttributeStyle(.fill)
if mediaStreams.has51AudioChannelLayout { }
Text("5.1") if viewModel.selectedMediaSource?.mediaStreams?.has71AudioChannelLayout == true {
.asAttributeStyle(.fill) Text("7.1")
} .asAttributeStyle(.fill)
}
if mediaStreams.has71AudioChannelLayout { case .subtitles:
Text("7.1") if viewModel.selectedMediaSource?.mediaStreams?.hasSubtitles == true {
.asAttributeStyle(.fill) Text("CC")
} .asAttributeStyle(.outline)
if mediaStreams.hasSubtitles {
Text("CC")
.asAttributeStyle(.outline)
}
} }
} }
.foregroundColor(Color(UIColor.darkGray))
} }
} }
} }

View File

@ -17,6 +17,12 @@ extension CustomizeViewsSettings {
@Injected(\.currentUserSession) @Injected(\.currentUserSession)
private var userSession private var userSession
@EnvironmentObject
private var router: CustomizeSettingsCoordinator.Router
@StoredValue(.User.itemViewAttributes)
private var itemViewAttributes
@StoredValue(.User.enableItemEditing) @StoredValue(.User.enableItemEditing)
private var enableItemEditing private var enableItemEditing
@StoredValue(.User.enableItemDeletion) @StoredValue(.User.enableItemDeletion)
@ -25,24 +31,24 @@ extension CustomizeViewsSettings {
private var enableCollectionManagement private var enableCollectionManagement
var body: some View { var body: some View {
if userSession?.user.permissions.items.canEditMetadata ?? false || Section(L10n.items) {
userSession?.user.permissions.items.canDelete ?? false ||
userSession?.user.permissions.items.canManageCollections ?? false
{
Section(L10n.items) { ChevronButton(L10n.mediaAttributes)
/// Enable Refreshing Items from All Visible LIbraries .onSelect {
if userSession?.user.permissions.items.canEditMetadata ?? false { router.route(to: \.itemViewAttributes, $itemViewAttributes)
Toggle(L10n.allowItemEditing, isOn: $enableItemEditing)
}
/// Enable Deleting Items from Approved Libraries
if userSession?.user.permissions.items.canDelete ?? false {
Toggle(L10n.allowItemDeletion, isOn: $enableItemDeletion)
}
/// Enable Refreshing & Deleting Collections
if userSession?.user.permissions.items.canManageCollections ?? false {
Toggle(L10n.allowCollectionManagement, isOn: $enableCollectionManagement)
} }
/// Enable Refreshing Items from All Visible LIbraries
if userSession?.user.permissions.items.canEditMetadata ?? false {
Toggle(L10n.allowItemEditing, isOn: $enableItemEditing)
}
/// Enable Deleting Items from Approved Libraries
if userSession?.user.permissions.items.canDelete ?? false {
Toggle(L10n.allowItemDeletion, isOn: $enableItemDeletion)
}
/// Enable Refreshing & Deleting Collections
if userSession?.user.permissions.items.canManageCollections ?? false {
Toggle(L10n.allowCollectionManagement, isOn: $enableCollectionManagement)
} }
} }
} }

View File

@ -33,6 +33,8 @@
4E17498F2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */; }; 4E17498F2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */; };
4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */; }; 4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */; };
4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */; }; 4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */; };
4E1A39332D56C84200BAC1C7 /* ItemViewAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1A39322D56C83E00BAC1C7 /* ItemViewAttributes.swift */; };
4E1A39342D56C84200BAC1C7 /* ItemViewAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1A39322D56C83E00BAC1C7 /* ItemViewAttributes.swift */; };
4E1AA0042D0640AA00524970 /* RemoteImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */; }; 4E1AA0042D0640AA00524970 /* RemoteImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */; };
4E1AA0052D0640AA00524970 /* RemoteImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */; }; 4E1AA0052D0640AA00524970 /* RemoteImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */; };
4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */; }; 4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */; };
@ -1285,6 +1287,7 @@
4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = "<group>"; }; 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = "<group>"; };
4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksView.swift; sourceTree = "<group>"; }; 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksView.swift; sourceTree = "<group>"; };
4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskRow.swift; sourceTree = "<group>"; }; 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskRow.swift; sourceTree = "<group>"; };
4E1A39322D56C83E00BAC1C7 /* ItemViewAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewAttributes.swift; sourceTree = "<group>"; };
4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfo.swift; sourceTree = "<group>"; }; 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfo.swift; sourceTree = "<group>"; };
4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeSettingsCoordinator.swift; sourceTree = "<group>"; }; 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeSettingsCoordinator.swift; sourceTree = "<group>"; };
4E2182E42CAF67EF0094806B /* PlayMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayMethod.swift; sourceTree = "<group>"; }; 4E2182E42CAF67EF0094806B /* PlayMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayMethod.swift; sourceTree = "<group>"; };
@ -3315,6 +3318,7 @@
4EFE0C7F2D02054300D4834D /* ItemArrayElements.swift */, 4EFE0C7F2D02054300D4834D /* ItemArrayElements.swift */,
E14EDECA2B8FB66F000F00A4 /* ItemFilter */, E14EDECA2B8FB66F000F00A4 /* ItemFilter */,
E1C925F328875037002A7A66 /* ItemViewType.swift */, E1C925F328875037002A7A66 /* ItemViewType.swift */,
4E1A39322D56C83E00BAC1C7 /* ItemViewAttributes.swift */,
E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */, E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */,
E1DE2B4E2B983F3200F6715F /* LibraryParent */, E1DE2B4E2B983F3200F6715F /* LibraryParent */,
4E2AC4C02C6C48EB00DD600D /* MediaComponents */, 4E2AC4C02C6C48EB00DD600D /* MediaComponents */,
@ -6158,6 +6162,7 @@
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
E148128628C15475003B8787 /* SortOrder+ItemSortOrder.swift in Sources */, E148128628C15475003B8787 /* SortOrder+ItemSortOrder.swift in Sources */,
E1CB75722C80E71800217C76 /* DirectPlayProfile.swift in Sources */, E1CB75722C80E71800217C76 /* DirectPlayProfile.swift in Sources */,
4E1A39332D56C84200BAC1C7 /* ItemViewAttributes.swift in Sources */,
E1E1E24E28DF8A2E000DF5FD /* PreferenceKeys.swift in Sources */, E1E1E24E28DF8A2E000DF5FD /* PreferenceKeys.swift in Sources */,
E1575E9B293E7B1E001665B1 /* EnvironmentValue+Keys.swift in Sources */, E1575E9B293E7B1E001665B1 /* EnvironmentValue+Keys.swift in Sources */,
E133328929538D8D00EE76AB /* Files.swift in Sources */, E133328929538D8D00EE76AB /* Files.swift in Sources */,
@ -6882,6 +6887,7 @@
E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */, E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */,
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */, 4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */,
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */, E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
4E1A39342D56C84200BAC1C7 /* ItemViewAttributes.swift in Sources */,
4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */, 4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */,
4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */, 4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */,
DFB7C3DF2C7AA43A00CE7CDC /* UserSignInState.swift in Sources */, DFB7C3DF2C7AA43A00CE7CDC /* UserSignInState.swift in Sources */,

View File

@ -9,14 +9,26 @@
import SwiftUI import SwiftUI
extension ItemView { extension ItemView {
struct AttributesHStack: View { struct AttributesHStack: View {
@ObservedObject @ObservedObject
var viewModel: ItemViewModel var viewModel: ItemViewModel
@StoredValue(.User.itemViewAttributes)
private var itemViewAttributes
var body: some View { var body: some View {
HStack { HStack {
ForEach(itemViewAttributes, id: \.self) { attribute in
getAttribute(attribute)
}
}
.foregroundStyle(Color(UIColor.darkGray))
}
@ViewBuilder
func getAttribute(_ attribute: ItemViewAttribute) -> some View {
switch attribute {
case .ratingCritics:
if let criticRating = viewModel.item.criticRating { if let criticRating = viewModel.item.criticRating {
HStack(spacing: 2) { HStack(spacing: 2) {
Group { Group {
@ -33,7 +45,7 @@ extension ItemView {
} }
.asAttributeStyle(.outline) .asAttributeStyle(.outline)
} }
case .ratingCommunity:
if let communityRating = viewModel.item.communityRating { if let communityRating = viewModel.item.communityRating {
HStack(spacing: 2) { HStack(spacing: 2) {
Image(systemName: "star.fill") Image(systemName: "star.fill")
@ -43,41 +55,35 @@ extension ItemView {
} }
.asAttributeStyle(.outline) .asAttributeStyle(.outline)
} }
case .ratingOfficial:
if let officialRating = viewModel.item.officialRating { if let officialRating = viewModel.item.officialRating {
Text(officialRating) Text(officialRating)
.asAttributeStyle(.outline) .asAttributeStyle(.outline)
} }
case .videoQuality:
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams { if viewModel.selectedMediaSource?.mediaStreams?.hasHDVideo == true {
Text("HD")
if mediaStreams.hasHDVideo { .asAttributeStyle(.fill)
Text("HD") }
.asAttributeStyle(.fill) if viewModel.selectedMediaSource?.mediaStreams?.has4KVideo == true {
} Text("4K")
.asAttributeStyle(.fill)
if mediaStreams.has4KVideo { }
Text("4K") case .audioChannels:
.asAttributeStyle(.fill) if viewModel.selectedMediaSource?.mediaStreams?.has51AudioChannelLayout == true {
} Text("5.1")
.asAttributeStyle(.fill)
if mediaStreams.has51AudioChannelLayout { }
Text("5.1") if viewModel.selectedMediaSource?.mediaStreams?.has71AudioChannelLayout == true {
.asAttributeStyle(.fill) Text("7.1")
} .asAttributeStyle(.fill)
}
if mediaStreams.has71AudioChannelLayout { case .subtitles:
Text("7.1") if viewModel.selectedMediaSource?.mediaStreams?.hasSubtitles == true {
.asAttributeStyle(.fill) Text("CC")
} .asAttributeStyle(.outline)
if mediaStreams.hasSubtitles {
Text("CC")
.asAttributeStyle(.outline)
}
} }
} }
.foregroundColor(Color(UIColor.darkGray))
} }
} }
} }

View File

@ -17,6 +17,12 @@ extension CustomizeViewsSettings {
@Injected(\.currentUserSession) @Injected(\.currentUserSession)
private var userSession private var userSession
@EnvironmentObject
private var router: SettingsCoordinator.Router
@StoredValue(.User.itemViewAttributes)
private var itemViewAttributes
@StoredValue(.User.enableItemEditing) @StoredValue(.User.enableItemEditing)
private var enableItemEditing private var enableItemEditing
@StoredValue(.User.enableItemDeletion) @StoredValue(.User.enableItemDeletion)
@ -25,39 +31,37 @@ extension CustomizeViewsSettings {
private var enableCollectionManagement private var enableCollectionManagement
var body: some View { var body: some View {
if userSession?.user.permissions.items.canEditMetadata ?? false Section(L10n.items) {
|| userSession?.user.permissions.items.canDelete ?? false
// || userSession?.user.permissions.items.canDownload ?? false ChevronButton(L10n.mediaAttributes)
|| userSession?.user.permissions.items.canManageCollections ?? false .onSelect {
// || userSession?.user.permissions.items.canManageLyrics ?? false router.route(to: \.itemViewAttributes, $itemViewAttributes)
// || userSession?.user.permissions.items.canManageSubtitles
{
Section(L10n.items) {
/// Enable Editing Items from All Visible LIbraries
if userSession?.user.permissions.items.canEditMetadata ?? false {
Toggle(L10n.allowItemEditing, isOn: $enableItemEditing)
} }
/// Enable Deleting Items from Approved Libraries
if userSession?.user.permissions.items.canDelete ?? false { /// Enable Editing Items from All Visible LIbraries
Toggle(L10n.allowItemDeletion, isOn: $enableItemDeletion) if userSession?.user.permissions.items.canEditMetadata ?? false {
} Toggle(L10n.allowItemEditing, isOn: $enableItemEditing)
/// Enable Downloading All Items
/* if userSession?.user.permissions.items.canDownload ?? false {
Toggle(L10n.allowItemDownloading, isOn: $enableItemDownloads)
} */
/// Enable Deleting or Editing Collections
if userSession?.user.permissions.items.canManageCollections ?? false {
Toggle(L10n.allowCollectionManagement, isOn: $enableCollectionManagement)
}
/// Manage Item Lyrics
/* if userSession?.user.permissions.items.canManageLyrics ?? false {
Toggle(L10n.allowLyricsManagement isOn: $enableLyricsManagement)
} */
/// Manage Item Subtitles
/* if userSession?.user.items.canManageSubtitles ?? false {
Toggle(L10n.allowSubtitleManagement, isOn: $enableSubtitleManagement)
} */
} }
/// Enable Deleting Items from Approved Libraries
if userSession?.user.permissions.items.canDelete ?? false {
Toggle(L10n.allowItemDeletion, isOn: $enableItemDeletion)
}
/// Enable Downloading All Items
/* if userSession?.user.permissions.items.canDownload ?? false {
Toggle(L10n.allowItemDownloading, isOn: $enableItemDownloads)
} */
/// Enable Deleting or Editing Collections
if userSession?.user.permissions.items.canManageCollections ?? false {
Toggle(L10n.allowCollectionManagement, isOn: $enableCollectionManagement)
}
/// Manage Item Lyrics
/* if userSession?.user.permissions.items.canManageLyrics ?? false {
Toggle(L10n.allowLyricsManagement isOn: $enableLyricsManagement)
} */
/// Manage Item Subtitles
/* if userSession?.user.items.canManageSubtitles ?? false {
Toggle(L10n.allowSubtitleManagement, isOn: $enableSubtitleManagement)
} */
} }
} }
} }

View File

@ -394,6 +394,9 @@
/// Community /// Community
"community" = "Community"; "community" = "Community";
/// Community rating
"communityRating" = "Community rating";
/// Compact /// Compact
"compact" = "Compact"; "compact" = "Compact";
@ -478,14 +481,17 @@
/// Creator /// Creator
"creator" = "Creator"; "creator" = "Creator";
/// Critic rating
"criticRating" = "Critic rating";
/// Critics /// Critics
"critics" = "Critics"; "critics" = "Critics";
/// Current /// Current
"current" = "Current"; "current" = "Current";
/// Current Password /// Current password
"currentPassword" = "Current Password"; "currentPassword" = "Current password";
/// Custom /// Custom
"custom" = "Custom"; "custom" = "Custom";
@ -520,11 +526,11 @@
/// Customize /// Customize
"customize" = "Customize"; "customize" = "Customize";
/// Custom Profile /// Custom profile
"customProfile" = "Custom Profile"; "customProfile" = "Custom profile";
/// Custom Rating /// Custom rating
"customRating" = "Custom Rating"; "customRating" = "Custom rating";
/// Custom sessions /// Custom sessions
"customSessions" = "Custom sessions"; "customSessions" = "Custom sessions";
@ -544,11 +550,11 @@
/// Date Added /// Date Added
"dateAdded" = "Date Added"; "dateAdded" = "Date Added";
/// Date Created /// Date created
"dateCreated" = "Date Created"; "dateCreated" = "Date created";
/// Date Modified /// Date modified
"dateModified" = "Date Modified"; "dateModified" = "Date modified";
/// Date of death /// Date of death
"dateOfDeath" = "Date of death"; "dateOfDeath" = "Date of death";
@ -1117,6 +1123,9 @@
/// Media Access /// Media Access
"mediaAccess" = "Media Access"; "mediaAccess" = "Media Access";
/// Media attributes
"mediaAttributes" = "Media attributes";
/// Media downloads /// Media downloads
"mediaDownloads" = "Media downloads"; "mediaDownloads" = "Media downloads";
@ -1240,8 +1249,8 @@
/// No title /// No title
"noTitle" = "No title"; "noTitle" = "No title";
/// Official Rating /// Official rating
"officialRating" = "Official Rating"; "officialRating" = "Official rating";
/// Offset /// Offset
"offset" = "Offset"; "offset" = "Offset";
@ -1285,8 +1294,8 @@
/// Parental controls /// Parental controls
"parentalControls" = "Parental controls"; "parentalControls" = "Parental controls";
/// Parental Rating /// Parental rating
"parentalRating" = "Parental Rating"; "parentalRating" = "Parental rating";
/// Password /// Password
"password" = "Password"; "password" = "Password";
@ -1423,8 +1432,8 @@
/// Random /// Random
"random" = "Random"; "random" = "Random";
/// Random Image /// Random image
"randomImage" = "Random Image"; "randomImage" = "Random image";
/// Rating /// Rating
"rating" = "Rating"; "rating" = "Rating";