Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tvOS] Mirror iOS Ratings + Attribute Settings #1422

Merged
merged 5 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Shared/Coordinators/CustomizeSettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ final class CustomizeSettingsCoordinator: NavigationCoordinatable {
@Route(.modal)
var indicatorSettings = makeIndicatorSettings
@Route(.modal)
var itemViewAttributes = makeItemViewAttributes
@Route(.push)
var listColumnSettings = makeListColumnSettings

func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
Expand All @@ -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 {
ListColumnsPickerView(selection: selection)
}
Expand Down
8 changes: 8 additions & 0 deletions Shared/Coordinators/SettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
@Route(.push)
var indicatorSettings = makeIndicatorSettings
@Route(.push)
var itemViewAttributes = makeItemViewAttributes
@Route(.push)
var serverConnection = makeServerConnection
@Route(.push)
var videoPlayerSettings = makeVideoPlayerSettings
Expand Down Expand Up @@ -149,6 +151,12 @@ final class SettingsCoordinator: NavigationCoordinatable {
IndicatorSettingsView()
}

@ViewBuilder
func makeItemViewAttributes(selection: Binding<[ItemViewAttribute]>) -> some View {
OrderedSectionSelectorView(selection: selection, sources: ItemViewAttribute.allCases)
.navigationTitle(L10n.mediaAttributes.localizedCapitalized)
}

@ViewBuilder
func makeServerConnection(server: ServerState) -> some View {
EditServerView(server: server)
Expand Down
34 changes: 34 additions & 0 deletions Shared/Objects/ItemViewAttributes.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
38 changes: 22 additions & 16 deletions Shared/Strings/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ internal enum L10n {
internal static let columns = L10n.tr("Localizable", "columns", fallback: "Columns")
/// 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
internal static let compact = L10n.tr("Localizable", "compact", fallback: "Compact")
/// Compact Logo
Expand Down Expand Up @@ -338,12 +340,14 @@ internal enum L10n {
}
/// 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
internal static let critics = L10n.tr("Localizable", "critics", fallback: "Critics")
/// Current
internal static let current = L10n.tr("Localizable", "current", fallback: "Current")
/// Current Password
internal static let currentPassword = L10n.tr("Localizable", "currentPassword", fallback: "Current Password")
/// Current password
internal static let currentPassword = L10n.tr("Localizable", "currentPassword", fallback: "Current password")
/// Custom
internal static let custom = L10n.tr("Localizable", "custom", fallback: "Custom")
/// Custom bitrate
Expand All @@ -368,10 +372,10 @@ internal enum L10n {
internal static let customFailedLogins = L10n.tr("Localizable", "customFailedLogins", fallback: "Custom failed logins")
/// Customize
internal static let customize = L10n.tr("Localizable", "customize", fallback: "Customize")
/// Custom Profile
internal static let customProfile = L10n.tr("Localizable", "customProfile", fallback: "Custom Profile")
/// Custom Rating
internal static let customRating = L10n.tr("Localizable", "customRating", fallback: "Custom Rating")
/// Custom profile
internal static let customProfile = L10n.tr("Localizable", "customProfile", fallback: "Custom profile")
/// Custom rating
internal static let customRating = L10n.tr("Localizable", "customRating", fallback: "Custom rating")
/// Custom sessions
internal static let customSessions = L10n.tr("Localizable", "customSessions", fallback: "Custom sessions")
/// Daily
Expand All @@ -384,10 +388,10 @@ internal enum L10n {
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.")
/// Date Added
internal static let dateAdded = L10n.tr("Localizable", "dateAdded", fallback: "Date Added")
/// Date Created
internal static let dateCreated = L10n.tr("Localizable", "dateCreated", fallback: "Date Created")
/// Date Modified
internal static let dateModified = L10n.tr("Localizable", "dateModified", fallback: "Date Modified")
/// Date created
internal static let dateCreated = L10n.tr("Localizable", "dateCreated", fallback: "Date created")
/// Date modified
internal static let dateModified = L10n.tr("Localizable", "dateModified", fallback: "Date modified")
/// Date of death
internal static let dateOfDeath = L10n.tr("Localizable", "dateOfDeath", fallback: "Date of death")
/// Dates
Expand Down Expand Up @@ -788,6 +792,8 @@ internal enum L10n {
internal static let media = L10n.tr("Localizable", "media", fallback: "Media")
/// 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
internal static let mediaDownloads = L10n.tr("Localizable", "mediaDownloads", fallback: "Media downloads")
/// Media playback
Expand Down Expand Up @@ -872,8 +878,8 @@ internal enum L10n {
}
/// No title
internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title")
/// Official Rating
internal static let officialRating = L10n.tr("Localizable", "officialRating", fallback: "Official Rating")
/// Official rating
internal static let officialRating = L10n.tr("Localizable", "officialRating", fallback: "Official rating")
/// Offset
internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset")
/// OK
Expand Down Expand Up @@ -902,8 +908,8 @@ internal enum L10n {
internal static let overview = L10n.tr("Localizable", "overview", fallback: "Overview")
/// Parental controls
internal static let parentalControls = L10n.tr("Localizable", "parentalControls", fallback: "Parental controls")
/// Parental Rating
internal static let parentalRating = L10n.tr("Localizable", "parentalRating", fallback: "Parental Rating")
/// Parental rating
internal static let parentalRating = L10n.tr("Localizable", "parentalRating", fallback: "Parental rating")
/// Password
internal static let password = L10n.tr("Localizable", "password", fallback: "Password")
/// User password has been changed.
Expand Down Expand Up @@ -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.")
/// Random
internal static let random = L10n.tr("Localizable", "random", fallback: "Random")
/// Random Image
internal static let randomImage = L10n.tr("Localizable", "randomImage", fallback: "Random Image")
/// Random image
internal static let randomImage = L10n.tr("Localizable", "randomImage", fallback: "Random image")
/// Rating
internal static let rating = L10n.tr("Localizable", "rating", fallback: "Rating")
/// %@ rating on a scale from 1 to 10.
Expand Down
8 changes: 8 additions & 0 deletions Shared/SwiftfinStore/StoredValue/StoredValues+User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,13 @@ extension StoredValues.Keys {
default: false
)
}

static var itemViewAttributes: Key<[ItemViewAttribute]> {
CurrentUserKey(
"itemViewAttributes",
domain: "itemViewAttributes",
default: ItemViewAttribute.allCases
)
}
}
}
90 changes: 61 additions & 29 deletions Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,81 @@
import SwiftUI

extension ItemView {

struct AttributesHStack: View {

@ObservedObject
var viewModel: ItemViewModel

@StoredValue(.User.itemViewAttributes)
private var itemViewAttributes

var body: some View {
HStack(spacing: 25) {

if let officialRating = viewModel.item.officialRating {
Text(officialRating)
.asAttributeStyle(.outline)
ForEach(itemViewAttributes, id: \.self) { attribute in
getAttribute(attribute)
}
}
.foregroundStyle(Color(UIColor.darkGray))
}

if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams {

if mediaStreams.hasHDVideo {
Text("HD")
.asAttributeStyle(.fill)
}

if mediaStreams.has4KVideo {
Text("4K")
.asAttributeStyle(.fill)
}

if mediaStreams.has51AudioChannelLayout {
Text("5.1")
.asAttributeStyle(.fill)
}
@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)

if mediaStreams.has71AudioChannelLayout {
Text("7.1")
.asAttributeStyle(.fill)
Text("\(criticRating, specifier: "%.0f")")
}
.asAttributeStyle(.outline)
}
case .ratingCommunity:
if let communityRating = viewModel.item.communityRating {
HStack(spacing: 2) {
Image(systemName: "star.fill")
.font(.caption2)

if mediaStreams.hasSubtitles {
Text("CC")
.asAttributeStyle(.outline)
Text("\(communityRating, specifier: "%.1f")")
}
.asAttributeStyle(.outline)
}
case .ratingOfficial:
if let officialRating = viewModel.item.officialRating {
Text(officialRating)
.asAttributeStyle(.outline)
}
case .videoQuality:
if viewModel.selectedMediaSource?.mediaStreams?.hasHDVideo == true {
Text("HD")
.asAttributeStyle(.fill)
}
if viewModel.selectedMediaSource?.mediaStreams?.has4KVideo == true {
Text("4K")
.asAttributeStyle(.fill)
}
case .audioChannels:
if viewModel.selectedMediaSource?.mediaStreams?.has51AudioChannelLayout == true {
Text("5.1")
.asAttributeStyle(.fill)
}
if viewModel.selectedMediaSource?.mediaStreams?.has71AudioChannelLayout == true {
Text("7.1")
.asAttributeStyle(.fill)
}
case .subtitles:
if viewModel.selectedMediaSource?.mediaStreams?.hasSubtitles == true {
Text("CC")
.asAttributeStyle(.outline)
}
}
.foregroundColor(Color(UIColor.darkGray))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ extension CustomizeViewsSettings {
@Injected(\.currentUserSession)
private var userSession

@EnvironmentObject
private var router: CustomizeSettingsCoordinator.Router

@StoredValue(.User.itemViewAttributes)
private var itemViewAttributes

@StoredValue(.User.enableItemEditing)
private var enableItemEditing
@StoredValue(.User.enableItemDeletion)
Expand All @@ -25,24 +31,24 @@ extension CustomizeViewsSettings {
private var enableCollectionManagement

var body: some View {
if userSession?.user.permissions.items.canEditMetadata ?? false ||
userSession?.user.permissions.items.canDelete ?? false ||
userSession?.user.permissions.items.canManageCollections ?? false
{

Section(L10n.items) {
/// 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)
Section(L10n.items) {

ChevronButton(L10n.mediaAttributes)
.onSelect {
router.route(to: \.itemViewAttributes, $itemViewAttributes)
}

/// 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)
}
}
}
Expand Down
Loading