[iOS] Admin Dashboard - Parental Ratings (#1353)
* Labels and Max Parental Rating * Parental Ratings * UnratedItem.displayTitle * Linting Fixes * Localizations, LearnMore, & cleaner grouping * Strings.swift * Review changes + Age Groups change * cleanup, use SeparatorVStack in LearnMoreButton * fix colors --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
8f05169097
commit
ba5c037ece
|
@ -10,17 +10,18 @@ import SwiftUI
|
||||||
|
|
||||||
// https://movingparts.io/variadic-views-in-swiftui
|
// https://movingparts.io/variadic-views-in-swiftui
|
||||||
|
|
||||||
/// An `HStack` that inserts an optional `separator` between views.
|
/// A `VStack` that inserts an optional `separator` between views.
|
||||||
///
|
///
|
||||||
/// - Note: Default spacing is removed. The separator view is responsible
|
/// - Note: Default spacing is removed. The separator view is responsible
|
||||||
/// for spacing.
|
/// for spacing.
|
||||||
struct SeparatorVStack<Content: View, Separator: View>: View {
|
struct SeparatorVStack<Content: View, Separator: View>: View {
|
||||||
|
|
||||||
private var content: () -> Content
|
private let alignment: HorizontalAlignment
|
||||||
private var separator: () -> Separator
|
private let content: () -> Content
|
||||||
|
private let separator: () -> Separator
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
_VariadicView.Tree(SeparatorVStackLayout(separator: separator)) {
|
_VariadicView.Tree(SeparatorVStackLayout(alignment: alignment, separator: separator)) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,10 +30,12 @@ struct SeparatorVStack<Content: View, Separator: View>: View {
|
||||||
extension SeparatorVStack {
|
extension SeparatorVStack {
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
alignment: HorizontalAlignment = .center,
|
||||||
@ViewBuilder separator: @escaping () -> Separator,
|
@ViewBuilder separator: @escaping () -> Separator,
|
||||||
@ViewBuilder content: @escaping () -> Content
|
@ViewBuilder content: @escaping () -> Content
|
||||||
) {
|
) {
|
||||||
self.init(
|
self.init(
|
||||||
|
alignment: alignment,
|
||||||
content: content,
|
content: content,
|
||||||
separator: separator
|
separator: separator
|
||||||
)
|
)
|
||||||
|
@ -43,14 +46,15 @@ extension SeparatorVStack {
|
||||||
|
|
||||||
struct SeparatorVStackLayout: _VariadicView_UnaryViewRoot {
|
struct SeparatorVStackLayout: _VariadicView_UnaryViewRoot {
|
||||||
|
|
||||||
var separator: () -> Separator
|
let alignment: HorizontalAlignment
|
||||||
|
let separator: () -> Separator
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func body(children: _VariadicView.Children) -> some View {
|
func body(children: _VariadicView.Children) -> some View {
|
||||||
|
|
||||||
let last = children.last?.id
|
let last = children.last?.id
|
||||||
|
|
||||||
localHStack {
|
VStack(alignment: alignment, spacing: 0) {
|
||||||
ForEach(children) { child in
|
ForEach(children) { child in
|
||||||
child
|
child
|
||||||
|
|
||||||
|
@ -60,12 +64,5 @@ extension SeparatorVStack {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func localHStack(@ViewBuilder content: @escaping () -> some View) -> some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,8 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var userPermissions = makeUserPermissions
|
var userPermissions = makeUserPermissions
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
|
var userParentalRatings = makeUserParentalRatings
|
||||||
|
@Route(.modal)
|
||||||
var resetUserPassword = makeResetUserPassword
|
var resetUserPassword = makeResetUserPassword
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var addServerUser = makeAddServerUser
|
var addServerUser = makeAddServerUser
|
||||||
|
@ -160,6 +162,12 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeUserParentalRatings(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
|
NavigationViewCoordinator {
|
||||||
|
ServerUserParentalRatingView(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func makeResetUserPassword(userID: String) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
func makeResetUserPassword(userID: String) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
NavigationViewCoordinator {
|
NavigationViewCoordinator {
|
||||||
ResetUserPasswordView(userID: userID, requiresCurrentPassword: false)
|
ResetUserPasswordView(userID: userID, requiresCurrentPassword: false)
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
extension UnratedItem: Displayable {
|
||||||
|
|
||||||
|
var displayTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .movie:
|
||||||
|
L10n.movies
|
||||||
|
case .trailer:
|
||||||
|
L10n.trailers
|
||||||
|
case .series:
|
||||||
|
L10n.tvShows
|
||||||
|
case .music:
|
||||||
|
L10n.music
|
||||||
|
case .book:
|
||||||
|
L10n.books
|
||||||
|
case .liveTvChannel:
|
||||||
|
L10n.liveTVChannels
|
||||||
|
case .liveTvProgram:
|
||||||
|
L10n.liveTVPrograms
|
||||||
|
case .channelContent:
|
||||||
|
L10n.channels
|
||||||
|
case .other:
|
||||||
|
L10n.other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,10 @@ internal enum L10n {
|
||||||
internal static let access = L10n.tr("Localizable", "access", fallback: "Access")
|
internal static let access = L10n.tr("Localizable", "access", fallback: "Access")
|
||||||
/// Accessibility
|
/// Accessibility
|
||||||
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
|
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
|
||||||
|
/// Access schedule
|
||||||
|
internal static let accessSchedule = L10n.tr("Localizable", "accessSchedule", fallback: "Access schedule")
|
||||||
|
/// Create an access schedule to limit access to certain hours.
|
||||||
|
internal static let accessScheduleDescription = L10n.tr("Localizable", "accessScheduleDescription", fallback: "Create an access schedule to limit access to certain hours.")
|
||||||
/// Active
|
/// Active
|
||||||
internal static let active = L10n.tr("Localizable", "active", fallback: "Active")
|
internal static let active = L10n.tr("Localizable", "active", fallback: "Active")
|
||||||
/// Active Devices
|
/// Active Devices
|
||||||
|
@ -50,6 +54,10 @@ internal enum L10n {
|
||||||
internal static let administrator = L10n.tr("Localizable", "administrator", fallback: "Administrator")
|
internal static let administrator = L10n.tr("Localizable", "administrator", fallback: "Administrator")
|
||||||
/// Advanced
|
/// Advanced
|
||||||
internal static let advanced = L10n.tr("Localizable", "advanced", fallback: "Advanced")
|
internal static let advanced = L10n.tr("Localizable", "advanced", fallback: "Advanced")
|
||||||
|
/// Age %@
|
||||||
|
internal static func agesGroup(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "agesGroup", String(describing: p1), fallback: "Age %@")
|
||||||
|
}
|
||||||
/// Aired
|
/// Aired
|
||||||
internal static let aired = L10n.tr("Localizable", "aired", fallback: "Aired")
|
internal static let aired = L10n.tr("Localizable", "aired", fallback: "Aired")
|
||||||
/// Air Time
|
/// Air Time
|
||||||
|
@ -60,6 +68,8 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
/// Album Artist
|
/// Album Artist
|
||||||
internal static let albumArtist = L10n.tr("Localizable", "albumArtist", fallback: "Album Artist")
|
internal static let albumArtist = L10n.tr("Localizable", "albumArtist", fallback: "Album Artist")
|
||||||
|
/// All Audiences
|
||||||
|
internal static let allAudiences = L10n.tr("Localizable", "allAudiences", fallback: "All Audiences")
|
||||||
/// View all past and present devices that have connected.
|
/// View all past and present devices that have connected.
|
||||||
internal static let allDevicesDescription = L10n.tr("Localizable", "allDevicesDescription", fallback: "View all past and present devices that have connected.")
|
internal static let allDevicesDescription = L10n.tr("Localizable", "allDevicesDescription", fallback: "View all past and present devices that have connected.")
|
||||||
/// All Genres
|
/// All Genres
|
||||||
|
@ -68,6 +78,10 @@ internal enum L10n {
|
||||||
internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media")
|
internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media")
|
||||||
/// Allow collection management
|
/// Allow collection management
|
||||||
internal static let allowCollectionManagement = L10n.tr("Localizable", "allowCollectionManagement", fallback: "Allow collection management")
|
internal static let allowCollectionManagement = L10n.tr("Localizable", "allowCollectionManagement", fallback: "Allow collection management")
|
||||||
|
/// Allowed tags
|
||||||
|
internal static let allowedTags = L10n.tr("Localizable", "allowedTags", fallback: "Allowed tags")
|
||||||
|
/// Only show media to this user with at least one of the specified tags.
|
||||||
|
internal static let allowedTagsDescription = L10n.tr("Localizable", "allowedTagsDescription", fallback: "Only show media to this user with at least one of the specified tags.")
|
||||||
/// Allow media item deletion
|
/// Allow media item deletion
|
||||||
internal static let allowItemDeletion = L10n.tr("Localizable", "allowItemDeletion", fallback: "Allow media item deletion")
|
internal static let allowItemDeletion = L10n.tr("Localizable", "allowItemDeletion", fallback: "Allow media item deletion")
|
||||||
/// Allow media item editing
|
/// Allow media item editing
|
||||||
|
@ -198,8 +212,18 @@ internal enum L10n {
|
||||||
internal static let bitrateTestDisclaimer = L10n.tr("Localizable", "bitrateTestDisclaimer", fallback: "Longer tests are more accurate but may result in a delayed playback.")
|
internal static let bitrateTestDisclaimer = L10n.tr("Localizable", "bitrateTestDisclaimer", fallback: "Longer tests are more accurate but may result in a delayed playback.")
|
||||||
/// bps
|
/// bps
|
||||||
internal static let bitsPerSecond = L10n.tr("Localizable", "bitsPerSecond", fallback: "bps")
|
internal static let bitsPerSecond = L10n.tr("Localizable", "bitsPerSecond", fallback: "bps")
|
||||||
|
/// Blocked tags
|
||||||
|
internal static let blockedTags = L10n.tr("Localizable", "blockedTags", fallback: "Blocked tags")
|
||||||
|
/// Hide media with at least one of the specified tags.
|
||||||
|
internal static let blockedTagsDescription = L10n.tr("Localizable", "blockedTagsDescription", fallback: "Hide media with at least one of the specified tags.")
|
||||||
|
/// Block unrated items
|
||||||
|
internal static let blockUnratedItems = L10n.tr("Localizable", "blockUnratedItems", fallback: "Block unrated items")
|
||||||
|
/// Block items from this user with no or unrecognized rating information.
|
||||||
|
internal static let blockUnratedItemsDescription = L10n.tr("Localizable", "blockUnratedItemsDescription", fallback: "Block items from this user with no or unrecognized rating information.")
|
||||||
/// Blue
|
/// Blue
|
||||||
internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue")
|
internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue")
|
||||||
|
/// Books
|
||||||
|
internal static let books = L10n.tr("Localizable", "books", fallback: "Books")
|
||||||
/// Bugs and Features
|
/// Bugs and Features
|
||||||
internal static let bugsAndFeatures = L10n.tr("Localizable", "bugsAndFeatures", fallback: "Bugs and Features")
|
internal static let bugsAndFeatures = L10n.tr("Localizable", "bugsAndFeatures", fallback: "Bugs and Features")
|
||||||
/// Buttons
|
/// Buttons
|
||||||
|
@ -736,6 +760,10 @@ internal enum L10n {
|
||||||
internal static let liveTV = L10n.tr("Localizable", "liveTV", fallback: "Live TV")
|
internal static let liveTV = L10n.tr("Localizable", "liveTV", fallback: "Live TV")
|
||||||
/// Live TV access
|
/// Live TV access
|
||||||
internal static let liveTvAccess = L10n.tr("Localizable", "liveTvAccess", fallback: "Live TV access")
|
internal static let liveTvAccess = L10n.tr("Localizable", "liveTvAccess", fallback: "Live TV access")
|
||||||
|
/// Live TV Channels
|
||||||
|
internal static let liveTVChannels = L10n.tr("Localizable", "liveTVChannels", fallback: "Live TV Channels")
|
||||||
|
/// Live TV Programs
|
||||||
|
internal static let liveTVPrograms = L10n.tr("Localizable", "liveTVPrograms", fallback: "Live TV Programs")
|
||||||
/// Live TV recording management
|
/// Live TV recording management
|
||||||
internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management")
|
internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management")
|
||||||
/// Loading
|
/// Loading
|
||||||
|
@ -782,6 +810,10 @@ internal enum L10n {
|
||||||
internal static let maximumSessions = L10n.tr("Localizable", "maximumSessions", fallback: "Maximum sessions")
|
internal static let maximumSessions = L10n.tr("Localizable", "maximumSessions", fallback: "Maximum sessions")
|
||||||
/// Maximum sessions policy
|
/// Maximum sessions policy
|
||||||
internal static let maximumSessionsPolicy = L10n.tr("Localizable", "maximumSessionsPolicy", fallback: "Maximum sessions policy")
|
internal static let maximumSessionsPolicy = L10n.tr("Localizable", "maximumSessionsPolicy", fallback: "Maximum sessions policy")
|
||||||
|
/// Maximum parental rating
|
||||||
|
internal static let maxParentalRating = L10n.tr("Localizable", "maxParentalRating", fallback: "Maximum parental rating")
|
||||||
|
/// Content with a higher rating will be hidden from this user.
|
||||||
|
internal static let maxParentalRatingDescription = L10n.tr("Localizable", "maxParentalRatingDescription", fallback: "Content with a higher rating will be hidden from this user.")
|
||||||
/// This setting may result in media failing to start playback.
|
/// This setting may result in media failing to start playback.
|
||||||
internal static let mayResultInPlaybackFailure = L10n.tr("Localizable", "mayResultInPlaybackFailure", fallback: "This setting may result in media failing to start playback.")
|
internal static let mayResultInPlaybackFailure = L10n.tr("Localizable", "mayResultInPlaybackFailure", fallback: "This setting may result in media failing to start playback.")
|
||||||
/// Media
|
/// Media
|
||||||
|
@ -818,6 +850,8 @@ internal enum L10n {
|
||||||
internal static func multipleUsers(_ p1: Int) -> String {
|
internal static func multipleUsers(_ p1: Int) -> String {
|
||||||
return L10n.tr("Localizable", "multipleUsers", p1, fallback: "%d users")
|
return L10n.tr("Localizable", "multipleUsers", p1, fallback: "%d users")
|
||||||
}
|
}
|
||||||
|
/// Music
|
||||||
|
internal static let music = L10n.tr("Localizable", "music", fallback: "Music")
|
||||||
/// MVC
|
/// MVC
|
||||||
internal static let mvc = L10n.tr("Localizable", "mvc", fallback: "MVC")
|
internal static let mvc = L10n.tr("Localizable", "mvc", fallback: "MVC")
|
||||||
/// Name
|
/// Name
|
||||||
|
@ -926,6 +960,8 @@ internal enum L10n {
|
||||||
internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String {
|
internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String {
|
||||||
return L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2), fallback: "Page %1$@ of %2$@")
|
return L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2), fallback: "Page %1$@ of %2$@")
|
||||||
}
|
}
|
||||||
|
/// 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
|
||||||
|
@ -1394,6 +1430,8 @@ internal enum L10n {
|
||||||
internal static let title = L10n.tr("Localizable", "title", fallback: "Title")
|
internal static let title = L10n.tr("Localizable", "title", fallback: "Title")
|
||||||
/// Too Many Redirects
|
/// Too Many Redirects
|
||||||
internal static let tooManyRedirects = L10n.tr("Localizable", "tooManyRedirects", fallback: "Too Many Redirects")
|
internal static let tooManyRedirects = L10n.tr("Localizable", "tooManyRedirects", fallback: "Too Many Redirects")
|
||||||
|
/// Trailers
|
||||||
|
internal static let trailers = L10n.tr("Localizable", "trailers", fallback: "Trailers")
|
||||||
/// Trailing Value
|
/// Trailing Value
|
||||||
internal static let trailingValue = L10n.tr("Localizable", "trailingValue", fallback: "Trailing Value")
|
internal static let trailingValue = L10n.tr("Localizable", "trailingValue", fallback: "Trailing Value")
|
||||||
/// Transcode
|
/// Transcode
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */; };
|
4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */; };
|
||||||
4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; };
|
4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; };
|
||||||
4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; };
|
4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; };
|
||||||
|
4E2470082D078DD7009139D8 /* ServerUserParentalRatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2470062D078DD7009139D8 /* ServerUserParentalRatingView.swift */; };
|
||||||
4E24ECFB2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */; };
|
4E24ECFB2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */; };
|
||||||
4E24ECFC2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */; };
|
4E24ECFC2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */; };
|
||||||
4E2AC4BE2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */; };
|
4E2AC4BE2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */; };
|
||||||
|
@ -100,6 +101,8 @@
|
||||||
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
|
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
|
||||||
4E63B9FA2C8A5BEF00C25378 /* AdminDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */; };
|
4E63B9FA2C8A5BEF00C25378 /* AdminDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */; };
|
||||||
4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; };
|
4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; };
|
||||||
|
4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */; };
|
||||||
|
4E656C312D0798AA00F993F3 /* ParentalRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */; };
|
||||||
4E6619FC2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */; };
|
4E6619FC2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */; };
|
||||||
4E6619FD2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */; };
|
4E6619FD2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */; };
|
||||||
4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A002CEFE39900025C99 /* EditMetadataView.swift */; };
|
4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A002CEFE39900025C99 /* EditMetadataView.swift */; };
|
||||||
|
@ -1181,6 +1184,7 @@
|
||||||
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>"; };
|
||||||
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>"; };
|
||||||
|
4E2470062D078DD7009139D8 /* ServerUserParentalRatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserParentalRatingView.swift; sourceTree = "<group>"; };
|
||||||
4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowCheckbox.swift; sourceTree = "<group>"; };
|
4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowCheckbox.swift; sourceTree = "<group>"; };
|
||||||
4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileAction.swift; sourceTree = "<group>"; };
|
4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileAction.swift; sourceTree = "<group>"; };
|
||||||
4E2AC4C12C6C491200DD600D /* AudoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudoCodec.swift; sourceTree = "<group>"; };
|
4E2AC4C12C6C491200DD600D /* AudoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudoCodec.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1228,6 +1232,7 @@
|
||||||
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
||||||
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = "<group>"; };
|
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = "<group>"; };
|
||||||
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = "<group>"; };
|
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
4E656C2F2D0798A900F993F3 /* ParentalRating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentalRating.swift; sourceTree = "<group>"; };
|
||||||
4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemEditorViewModel.swift; sourceTree = "<group>"; };
|
4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemEditorViewModel.swift; sourceTree = "<group>"; };
|
||||||
4E661A002CEFE39900025C99 /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
|
4E661A002CEFE39900025C99 /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
|
||||||
4E661A042CEFE46300025C99 /* DateSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSection.swift; sourceTree = "<group>"; };
|
4E661A042CEFE46300025C99 /* DateSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSection.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2142,6 +2147,14 @@
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4E2470072D078DD7009139D8 /* ServerUserParentalRatingView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E2470062D078DD7009139D8 /* ServerUserParentalRatingView.swift */,
|
||||||
|
);
|
||||||
|
path = ServerUserParentalRatingView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4E2AC4C02C6C48EB00DD600D /* MediaComponents */ = {
|
4E2AC4C02C6C48EB00DD600D /* MediaComponents */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2299,6 +2312,7 @@
|
||||||
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
||||||
4E182C9A2C94991800FBEFD5 /* ServerTasksView */,
|
4E182C9A2C94991800FBEFD5 /* ServerTasksView */,
|
||||||
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
||||||
|
4E2470072D078DD7009139D8 /* ServerUserParentalRatingView */,
|
||||||
4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */,
|
4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */,
|
||||||
4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */,
|
4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */,
|
||||||
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */,
|
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */,
|
||||||
|
@ -4234,6 +4248,7 @@
|
||||||
E122A9122788EAAD0060FA63 /* MediaStream.swift */,
|
E122A9122788EAAD0060FA63 /* MediaStream.swift */,
|
||||||
4E661A2D2CEFE77700025C99 /* MetadataField.swift */,
|
4E661A2D2CEFE77700025C99 /* MetadataField.swift */,
|
||||||
E1AD105E26D9ADDD003E4A08 /* NameGuidPair.swift */,
|
E1AD105E26D9ADDD003E4A08 /* NameGuidPair.swift */,
|
||||||
|
4E656C2F2D0798A900F993F3 /* ParentalRating.swift */,
|
||||||
4EFE0C7C2D0156A500D4834D /* PersonKind.swift */,
|
4EFE0C7C2D0156A500D4834D /* PersonKind.swift */,
|
||||||
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */,
|
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */,
|
||||||
4E2182E42CAF67EF0094806B /* PlayMethod.swift */,
|
4E2182E42CAF67EF0094806B /* PlayMethod.swift */,
|
||||||
|
@ -5162,6 +5177,7 @@
|
||||||
E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */,
|
E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */,
|
||||||
E1575E69293E77B5001665B1 /* ItemSortBy.swift in Sources */,
|
E1575E69293E77B5001665B1 /* ItemSortBy.swift in Sources */,
|
||||||
E1B490482967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
E1B490482967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
||||||
|
4E656C312D0798AA00F993F3 /* ParentalRating.swift in Sources */,
|
||||||
E1DC9845296DECB600982F06 /* ProgressIndicator.swift in Sources */,
|
E1DC9845296DECB600982F06 /* ProgressIndicator.swift in Sources */,
|
||||||
E1C925F928875647002A7A66 /* LatestInLibraryView.swift in Sources */,
|
E1C925F928875647002A7A66 /* LatestInLibraryView.swift in Sources */,
|
||||||
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||||
|
@ -5384,6 +5400,7 @@
|
||||||
6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */,
|
6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */,
|
||||||
E17AC96D2954E9CA003D2BC2 /* DownloadListView.swift in Sources */,
|
E17AC96D2954E9CA003D2BC2 /* DownloadListView.swift in Sources */,
|
||||||
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */,
|
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */,
|
||||||
|
4E2470082D078DD7009139D8 /* ServerUserParentalRatingView.swift in Sources */,
|
||||||
E1A1528828FD229500600579 /* ChevronButton.swift in Sources */,
|
E1A1528828FD229500600579 /* ChevronButton.swift in Sources */,
|
||||||
E1CB75732C80E71800217C76 /* DirectPlayProfile.swift in Sources */,
|
E1CB75732C80E71800217C76 /* DirectPlayProfile.swift in Sources */,
|
||||||
E1B490472967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
E1B490472967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
||||||
|
@ -5546,6 +5563,7 @@
|
||||||
4E2AC4C52C6C492700DD600D /* MediaContainer.swift in Sources */,
|
4E2AC4C52C6C492700DD600D /* MediaContainer.swift in Sources */,
|
||||||
4E2AC4CB2C6C494E00DD600D /* VideoCodec.swift in Sources */,
|
4E2AC4CB2C6C494E00DD600D /* VideoCodec.swift in Sources */,
|
||||||
E1EA09692BED78BB004CDE76 /* UserAccessPolicy.swift in Sources */,
|
E1EA09692BED78BB004CDE76 /* UserAccessPolicy.swift in Sources */,
|
||||||
|
4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */,
|
||||||
E18E0204288749200022598C /* RowDivider.swift in Sources */,
|
E18E0204288749200022598C /* RowDivider.swift in Sources */,
|
||||||
E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */,
|
E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */,
|
||||||
E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */,
|
E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */,
|
||||||
|
|
|
@ -42,7 +42,9 @@ struct LearnMoreButton: View {
|
||||||
private var learnMoreView: some View {
|
private var learnMoreView: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
SeparatorVStack(alignment: .leading) {
|
||||||
|
Divider()
|
||||||
|
} content: {
|
||||||
ForEach(items) { content in
|
ForEach(items) { content in
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text(content.title)
|
Text(content.title)
|
||||||
|
@ -53,10 +55,10 @@ struct LearnMoreButton: View {
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Divider()
|
.padding(.vertical, 16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.edgePadding()
|
.edgePadding(.horizontal)
|
||||||
}
|
}
|
||||||
.navigationTitle(title)
|
.navigationTitle(title)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
@ -64,5 +66,6 @@ struct LearnMoreButton: View {
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.foregroundStyle(Color.primary, Color.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ServerUserDetailsView: View {
|
struct ServerUserDetailsView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: AdminDashboardCoordinator.Router
|
private var router: AdminDashboardCoordinator.Router
|
||||||
|
|
||||||
|
@ -68,31 +67,27 @@ struct ServerUserDetailsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section("Parental controls") {
|
Section(L10n.parentalControls) {
|
||||||
// TODO: Allow items SDK 10.10 - allowedTags
|
// TODO: Access Schedules - accessSchedules
|
||||||
ChevronButton("Allow items")
|
/* ChevronButton("Access schedule")
|
||||||
.onSelect {
|
|
||||||
router.route(to: \.userAllowedTags, viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Block items - blockedTags
|
|
||||||
ChevronButton("Block items")
|
|
||||||
.onSelect {
|
|
||||||
router.route(to: \.userBlockedTags, viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Access Schedules - accessSchedules
|
|
||||||
ChevronButton("Access schedule")
|
|
||||||
.onSelect {
|
.onSelect {
|
||||||
router.route(to: \.userAccessSchedules, viewModel)
|
router.route(to: \.userAccessSchedules, viewModel)
|
||||||
}
|
}
|
||||||
|
// TODO: Allow items SDK 10.10 - allowedTags
|
||||||
// TODO: Parental Rating - maxParentalRating, blockUnratedItems
|
ChevronButton("Allow items")
|
||||||
ChevronButton("Parental rating")
|
.onSelect {
|
||||||
.onSelect {
|
router.route(to: \.userAllowedTags, viewModel)
|
||||||
router.route(to: \.userParentalRating, viewModel)
|
}
|
||||||
}
|
// TODO: Block items - blockedTags
|
||||||
} */
|
ChevronButton("Block items")
|
||||||
|
.onSelect {
|
||||||
|
router.route(to: \.userBlockedTags, viewModel)
|
||||||
|
}*/
|
||||||
|
ChevronButton(L10n.ratings)
|
||||||
|
.onSelect {
|
||||||
|
router.route(to: \.userParentalRatings, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.user)
|
.navigationTitle(L10n.user)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ServerUserParentalRatingView: View {
|
||||||
|
|
||||||
|
// MARK: - Observed, State, & Environment Objects
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var viewModel: ServerUserAdminViewModel
|
||||||
|
@ObservedObject
|
||||||
|
private var parentalRatingsViewModel = ParentalRatingsViewModel()
|
||||||
|
|
||||||
|
// MARK: - Policy Variable
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var tempPolicy: UserPolicy
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init(viewModel: ServerUserAdminViewModel) {
|
||||||
|
self._viewModel = StateObject(wrappedValue: viewModel)
|
||||||
|
self.tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
contentView
|
||||||
|
.navigationTitle(L10n.parentalRating)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarCloseButton {
|
||||||
|
router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
.topBarTrailing {
|
||||||
|
if viewModel.backgroundStates.contains(.updating) {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
Button(L10n.save) {
|
||||||
|
if tempPolicy != viewModel.user.policy {
|
||||||
|
viewModel.send(.updatePolicy(tempPolicy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.disabled(viewModel.user.policy == tempPolicy)
|
||||||
|
}
|
||||||
|
.onFirstAppear {
|
||||||
|
parentalRatingsViewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
.onReceive(viewModel.events) { event in
|
||||||
|
switch event {
|
||||||
|
case let .error(eventError):
|
||||||
|
UIDevice.feedback(.error)
|
||||||
|
error = eventError
|
||||||
|
case .updated:
|
||||||
|
UIDevice.feedback(.success)
|
||||||
|
router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.errorMessage($error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Content View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var contentView: some View {
|
||||||
|
List {
|
||||||
|
maxParentalRatingsView
|
||||||
|
blockUnratedItemsView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Maximum Parental Ratings View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var maxParentalRatingsView: some View {
|
||||||
|
Section {
|
||||||
|
Picker(L10n.parentalRating, selection: $tempPolicy.maxParentalRating) {
|
||||||
|
ForEach(parentalRatingGroups, id: \.value) { rating in
|
||||||
|
Text(rating.name ?? L10n.unknown)
|
||||||
|
.tag(rating.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text(L10n.maxParentalRating)
|
||||||
|
} footer: {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(L10n.maxParentalRatingDescription)
|
||||||
|
|
||||||
|
LearnMoreButton(L10n.parentalRating) {
|
||||||
|
parentalRatingLearnMore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Block Unrated Items View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var blockUnratedItemsView: some View {
|
||||||
|
Section {
|
||||||
|
ForEach(UnratedItem.allCases.sorted(using: \.displayTitle), id: \.self) { item in
|
||||||
|
Toggle(
|
||||||
|
item.displayTitle,
|
||||||
|
isOn: $tempPolicy.blockUnratedItems
|
||||||
|
.coalesce([])
|
||||||
|
.contains(item)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text(L10n.blockUnratedItems)
|
||||||
|
} footer: {
|
||||||
|
Text(L10n.blockUnratedItemsDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Parental Rating Groups
|
||||||
|
|
||||||
|
private var parentalRatingGroups: [ParentalRating] {
|
||||||
|
let groups = Dictionary(
|
||||||
|
grouping: parentalRatingsViewModel.parentalRatings
|
||||||
|
) {
|
||||||
|
$0.value ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupedRatings = groups.map { key, group in
|
||||||
|
if key < 100 {
|
||||||
|
if key == 0 {
|
||||||
|
return ParentalRating(name: L10n.allAudiences, value: key)
|
||||||
|
} else {
|
||||||
|
return ParentalRating(name: L10n.agesGroup(key), value: key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Concatenate all 100+ ratings at the same value with '/' but as of 10.10 there should be none.
|
||||||
|
let name = group
|
||||||
|
.compactMap(\.name)
|
||||||
|
.sorted()
|
||||||
|
.joined(separator: " / ")
|
||||||
|
|
||||||
|
return ParentalRating(name: name, value: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sorted(using: \.value)
|
||||||
|
|
||||||
|
let unrated = ParentalRating(name: L10n.none, value: nil)
|
||||||
|
groupedRatings.insert(unrated, at: 0)
|
||||||
|
|
||||||
|
return groupedRatings
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Parental Rating Learn More
|
||||||
|
|
||||||
|
private var parentalRatingLearnMore: [TextPair] {
|
||||||
|
let groups = Dictionary(
|
||||||
|
grouping: parentalRatingsViewModel.parentalRatings
|
||||||
|
) {
|
||||||
|
$0.value ?? 0
|
||||||
|
}
|
||||||
|
.sorted(using: \.key)
|
||||||
|
|
||||||
|
let groupedRatings = groups.compactMap { key, group in
|
||||||
|
let matchingRating = parentalRatingGroups.first { $0.value == key }
|
||||||
|
|
||||||
|
let name = group
|
||||||
|
.compactMap(\.name)
|
||||||
|
.sorted()
|
||||||
|
.joined(separator: "\n")
|
||||||
|
|
||||||
|
return TextPair(
|
||||||
|
title: matchingRating?.name ?? L10n.none,
|
||||||
|
subtitle: name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupedRatings
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,6 @@ struct PlaybackQualitySettingsView: View {
|
||||||
subtitle: L10n.bitrateMaxDescription(PlaybackBitrate.max.rawValue.formatted(.bitRate))
|
subtitle: L10n.bitrateMaxDescription(PlaybackBitrate.max.rawValue.formatted(.bitRate))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.foregroundStyle(.foreground, .primary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.none, value: appMaximumBitrate)
|
.animation(.none, value: appMaximumBitrate)
|
||||||
|
@ -99,7 +98,6 @@ struct PlaybackQualitySettingsView: View {
|
||||||
subtitle: L10n.customDescription
|
subtitle: L10n.customDescription
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.foregroundStyle(.foreground, .primary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2049,6 +2049,70 @@
|
||||||
// Represents a translator
|
// Represents a translator
|
||||||
"translator" = "Translator";
|
"translator" = "Translator";
|
||||||
|
|
||||||
|
// Parental controls - Section Title
|
||||||
|
// Parental controls section & view titles
|
||||||
|
"parentalControls" = "Parental controls";
|
||||||
|
|
||||||
|
// Block unrated items - Section Title
|
||||||
|
// Parental ratings block unrated items section
|
||||||
|
"blockUnratedItems" = "Block unrated items";
|
||||||
|
|
||||||
|
// Block unrated items - Footer
|
||||||
|
// Parental ratings block unrated items description
|
||||||
|
"blockUnratedItemsDescription" = "Block items from this user with no or unrecognized rating information.";
|
||||||
|
|
||||||
|
// Maximum parental rating - Section Title
|
||||||
|
// Parental ratings maximum parental rating section
|
||||||
|
"maxParentalRating" = "Maximum parental rating";
|
||||||
|
|
||||||
|
// Maximum parental rating - Footer
|
||||||
|
// Parental ratings maximum parental rating description
|
||||||
|
"maxParentalRatingDescription" = "Content with a higher rating will be hidden from this user.";
|
||||||
|
|
||||||
|
// Allowed tags - View Title
|
||||||
|
// Parental ratings section for allowed tags
|
||||||
|
"allowedTags" = "Allowed tags";
|
||||||
|
|
||||||
|
// Allowed tags - Footer
|
||||||
|
// Parental ratings description for allowed tags
|
||||||
|
"allowedTagsDescription" = "Only show media to this user with at least one of the specified tags.";
|
||||||
|
|
||||||
|
// Blocked tags - View Title
|
||||||
|
// Parental ratings section for blocked tags
|
||||||
|
"blockedTags" = "Blocked tags";
|
||||||
|
|
||||||
|
// Blocked tags - Footer
|
||||||
|
// Parental ratings description for blocked tags
|
||||||
|
"blockedTagsDescription" = "Hide media with at least one of the specified tags.";
|
||||||
|
|
||||||
|
// Access Schedule - View Title
|
||||||
|
// Parental ratings section for blocked titles
|
||||||
|
"accessSchedule" = "Access schedule";
|
||||||
|
|
||||||
|
// Access Schedule - Footer
|
||||||
|
// Parental ratings section for allowed titles
|
||||||
|
"accessScheduleDescription" = "Create an access schedule to limit access to certain hours.";
|
||||||
|
|
||||||
|
// Trailers - Section Title
|
||||||
|
// Title for content classified as trailers
|
||||||
|
"trailers" = "Trailers";
|
||||||
|
|
||||||
|
// Music - Section Title
|
||||||
|
// Title for content classified as music
|
||||||
|
"music" = "Music";
|
||||||
|
|
||||||
|
// Books - Section Title
|
||||||
|
// Title for content classified as books
|
||||||
|
"books" = "Books";
|
||||||
|
|
||||||
|
// Live TV Channels - Section Title
|
||||||
|
// Title for content classified as live TV channels
|
||||||
|
"liveTVChannels" = "Live TV Channels";
|
||||||
|
|
||||||
|
// Live TV Programs - Section Title
|
||||||
|
// Title for content classified as live TV programs
|
||||||
|
"liveTVPrograms" = "Live TV Programs";
|
||||||
|
|
||||||
// Loading User Failed - Error Message
|
// Loading User Failed - Error Message
|
||||||
// Displayed when loading user data fails
|
// Displayed when loading user data fails
|
||||||
"loadingUserFailed" = "Loading user failed";
|
"loadingUserFailed" = "Loading user failed";
|
||||||
|
@ -2180,3 +2244,11 @@
|
||||||
// Server Already Connected - Error Message
|
// Server Already Connected - Error Message
|
||||||
// Indicates that the specified server is already connected
|
// Indicates that the specified server is already connected
|
||||||
"serverAlreadyConnected" = "%@ is already connected.";
|
"serverAlreadyConnected" = "%@ is already connected.";
|
||||||
|
|
||||||
|
// All Audiences - Group Name
|
||||||
|
// Label for content suitable for all audiences
|
||||||
|
"allAudiences" = "All Audiences";
|
||||||
|
|
||||||
|
// Ages Group - Group Name
|
||||||
|
// Label for content suitable for a specific age group
|
||||||
|
"agesGroup" = "Age %@";
|
||||||
|
|
Loading…
Reference in New Issue