[iOS & tvOS] Cleanup Permission Validation (#1499)

* Move permissions to centralized spot

* Move `identifiableTypes` to `BaseItemKind`. Use `showEditMenu`

* Cleanup showMenu options for iOS and tvOS. Metadata allows Subtitle, Lyrics, and Collection edits as well.

* Comment out Lyrics and Subtitles with a TODO for when they are available.

* Update BaseItemKind.swift

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>

* Review Revisions

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-04-24 10:06:18 -06:00 committed by GitHub
parent 8194057d40
commit a3dab2e165
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 194 additions and 86 deletions

View File

@ -29,3 +29,10 @@ extension BaseItemKind: ItemFilter {
rawValue rawValue
} }
} }
extension BaseItemKind {
static var itemIdentifiableCases: [BaseItemKind] {
[.boxSet, .movie, .person, .series]
}
}

View File

@ -19,12 +19,17 @@ struct UserPermissions {
} }
struct UserItemPermissions { struct UserItemPermissions {
/// This user has server permissions to delete items
let canDelete: Bool let canDelete: Bool
/// This user has server permissions to download items
let canDownload: Bool let canDownload: Bool
/// This user has server permissions to edit items' metadata
let canEditMetadata: Bool let canEditMetadata: Bool
/// This user has server permissions to edit items' subtitles
let canManageSubtitles: Bool let canManageSubtitles: Bool
/// This user has server permissions to edit collection
let canManageCollections: Bool let canManageCollections: Bool
/// This user has server permissions to edit items' lyrics
let canManageLyrics: Bool let canManageLyrics: Bool
init(_ policy: UserPolicy?, isAdministrator: Bool) { init(_ policy: UserPolicy?, isAdministrator: Bool) {
@ -35,5 +40,66 @@ struct UserPermissions {
self.canManageCollections = isAdministrator || policy?.enableCollectionManagement ?? false self.canManageCollections = isAdministrator || policy?.enableCollectionManagement ?? false
self.canManageLyrics = isAdministrator || policy?.enableSubtitleManagement ?? false self.canManageLyrics = isAdministrator || policy?.enableSubtitleManagement ?? false
} }
// MARK: - Item Specific Validation
/// Does this user have permission to delete this item?
func canDelete(item: BaseItemDto) -> Bool {
switch item.type {
case .playlist:
/// Playlists can only be edited by owners who can also delete
return item.canDelete == true
case .boxSet:
return canManageCollections
&& StoredValues[.User.enableCollectionManagement]
&& item.canDelete == true
default:
return canDelete
&& StoredValues[.User.enableItemDeletion]
&& item.canDelete == true
}
}
/// Does this user have permission to download this item?
func canDownload(item: BaseItemDto) -> Bool {
canDownload && item.canDownload == true
}
/// Does this user have permission to edit this item's metadata?
func canEditMetadata(item: BaseItemDto) -> Bool {
switch item.type {
case .playlist:
/// Playlists can only be edited by owners who can also delete
return item.canDelete == true
case .boxSet:
return (canManageCollections || canEditMetadata)
&& StoredValues[.User.enableCollectionManagement]
default:
return canEditMetadata
&& StoredValues[.User.enableItemEditing]
}
}
/// Does this user have permission to edit this item's subtitles?
func canManageSubtitles(item: BaseItemDto) -> Bool {
switch item.type {
case .episode, .movie, .musicVideo, .trailer, .video:
return (canManageSubtitles || canEditMetadata)
&& StoredValues[.User.enableItemEditing]
default:
return false
}
}
/// Does this user have permission to edit this item's lyrics?
func canManageLyrics(item: BaseItemDto) -> Bool {
switch item.type {
case .audio:
return (canManageLyrics || canEditMetadata)
&& StoredValues[.User.enableItemEditing]
default:
return false
}
}
} }
} }

View File

@ -540,8 +540,8 @@ internal enum L10n {
internal static let dvd = L10n.tr("Localizable", "dvd", fallback: "DVD") internal static let dvd = L10n.tr("Localizable", "dvd", fallback: "DVD")
/// Edit /// Edit
internal static let edit = L10n.tr("Localizable", "edit", fallback: "Edit") internal static let edit = L10n.tr("Localizable", "edit", fallback: "Edit")
/// Edit Collections /// Edit collections
internal static let editCollections = L10n.tr("Localizable", "editCollections", fallback: "Edit Collections") internal static let editCollections = L10n.tr("Localizable", "editCollections", fallback: "Edit collections")
/// Edit media /// Edit media
internal static let editMedia = L10n.tr("Localizable", "editMedia", fallback: "Edit media") internal static let editMedia = L10n.tr("Localizable", "editMedia", fallback: "Edit media")
/// Editor /// Editor

View File

@ -47,21 +47,19 @@ extension ItemView {
// MARK: - Can Delete Item // MARK: - Can Delete Item
private var canDelete: Bool { private var canDelete: Bool {
if viewModel.item.type == .boxSet { viewModel.userSession.user.permissions.items.canDelete(item: viewModel.item)
return enableCollectionManagement && viewModel.item.canDelete ?? false
} else {
return enableItemDeletion && viewModel.item.canDelete ?? false
}
} }
// MARK: - Refresh Item // MARK: - Can Refresh Item
private var canRefresh: Bool { private var canRefresh: Bool {
if viewModel.item.type == .boxSet { viewModel.userSession.user.permissions.items.canEditMetadata(item: viewModel.item)
return enableCollectionManagement }
} else {
return enableItemEditing // MARK: - Deletion or Refreshing is Enabled
}
private var enableMenu: Bool {
canDelete || canRefresh
} }
// MARK: - Has Trailers // MARK: - Has Trailers
@ -131,15 +129,17 @@ extension ItemView {
// MARK: Advanced Options // MARK: Advanced Options
if canRefresh || canDelete { if enableMenu {
ActionButton(L10n.advanced, icon: "ellipsis", isCompact: true) { ActionButton(L10n.advanced, icon: "ellipsis", isCompact: true) {
if canRefresh { if canRefresh {
RefreshMetadataButton(item: viewModel.item) RefreshMetadataButton(item: viewModel.item)
} }
if canDelete { if canDelete {
Button(L10n.delete, systemImage: "trash", role: .destructive) { Section {
showConfirmationDialog = true Button(L10n.delete, systemImage: "trash", role: .destructive) {
showConfirmationDialog = true
}
} }
} }
} }

View File

@ -41,18 +41,18 @@ extension CustomizeViewsSettings {
ListRowMenu(L10n.enabledTrailers, selection: $enabledTrailers) ListRowMenu(L10n.enabledTrailers, selection: $enabledTrailers)
/// Enable Refreshing & Deleting Collections
if userSession?.user.permissions.items.canManageCollections == true {
Toggle(L10n.editCollections, isOn: $enableCollectionManagement)
}
/// Enable Refreshing Items from All Visible LIbraries /// Enable Refreshing Items from All Visible LIbraries
if userSession?.user.permissions.items.canEditMetadata ?? false { if userSession?.user.permissions.items.canEditMetadata == true {
Toggle(L10n.editMedia, isOn: $enableItemEditing) Toggle(L10n.editMedia, isOn: $enableItemEditing)
} }
/// Enable Deleting Items from Approved Libraries /// Enable Deleting Items from Approved Libraries
if userSession?.user.permissions.items.canDelete ?? false { if userSession?.user.permissions.items.canDelete == true {
Toggle(L10n.deleteMedia, isOn: $enableItemDeletion) Toggle(L10n.deleteMedia, isOn: $enableItemDeletion)
} }
/// Enable Refreshing & Deleting Collections
if userSession?.user.permissions.items.canManageCollections ?? false {
Toggle(L10n.editCollections, isOn: $enableCollectionManagement)
}
} }
} }
} }

View File

@ -12,15 +12,30 @@ import SwiftUI
struct ItemEditorView: View { struct ItemEditorView: View {
@Injected(\.currentUserSession)
private var userSession
@EnvironmentObject @EnvironmentObject
private var router: ItemEditorCoordinator.Router private var router: ItemEditorCoordinator.Router
@ObservedObject @ObservedObject
var viewModel: ItemViewModel var viewModel: ItemViewModel
// MARK: - Can Edit Metadata
private var canEditMetadata: Bool {
viewModel.userSession.user.permissions.items.canEditMetadata(item: viewModel.item) == true
}
// MARK: - Can Manage Subtitles
private var canManageSubtitles: Bool {
viewModel.userSession.user.permissions.items.canManageSubtitles(item: viewModel.item) == true
}
// MARK: - Can Manage Lyrics
private var canManageLyrics: Bool {
viewModel.userSession.user.permissions.items.canManageLyrics(item: viewModel.item) == true
}
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
@ -48,9 +63,24 @@ struct ItemEditorView: View {
description: viewModel.item.path description: viewModel.item.path
) )
refreshButtonView /// Hide metadata options to Lyric/Subtitle only users
if canEditMetadata {
editView refreshButtonView
Section(L10n.edit) {
editMetadataView
editTextView
}
editComponentsView
} /* else if canManageSubtitles || canManageLyrics {
// TODO: Enable when Subtitle / Lyric Editing is added
Section(L10n.edit) {
editTextView
}
}*/
} }
} }
@ -70,7 +100,6 @@ struct ItemEditorView: View {
private var refreshButtonView: some View { private var refreshButtonView: some View {
Section { Section {
RefreshMetadataButton(item: viewModel.item) RefreshMetadataButton(item: viewModel.item)
.environment(\.isEnabled, userSession?.user.permissions.isAdministrator ?? false)
} footer: { } footer: {
LearnMoreButton(L10n.metadata) { LearnMoreButton(L10n.metadata) {
TextPair( TextPair(
@ -93,24 +122,46 @@ struct ItemEditorView: View {
} }
} }
// MARK: - Editable Routing Buttons // MARK: - Editable Metadata Routing Buttons
@ViewBuilder @ViewBuilder
private var editView: some View { private var editMetadataView: some View {
Section(L10n.edit) {
if [.boxSet, .movie, .person, .series].contains(viewModel.item.type) { if let itemKind = viewModel.item.type,
ChevronButton(L10n.identify) { BaseItemKind.itemIdentifiableCases.contains(itemKind)
router.route(to: \.identifyItem, viewModel.item) {
} ChevronButton(L10n.identify) {
} router.route(to: \.identifyItem, viewModel.item)
ChevronButton(L10n.images) {
router.route(to: \.editImages, ItemImagesViewModel(item: viewModel.item))
}
ChevronButton(L10n.metadata) {
router.route(to: \.editMetadata, viewModel.item)
} }
} }
ChevronButton(L10n.images) {
router.route(to: \.editImages, ItemImagesViewModel(item: viewModel.item))
}
ChevronButton(L10n.metadata) {
router.route(to: \.editMetadata, viewModel.item)
}
}
// MARK: - Editable Text Routing Buttons
@ViewBuilder
private var editTextView: some View {
if canManageLyrics {
// ChevronButton(L10n.lyrics) {
// router.route(to: \.editImages, ItemImagesViewModel(item: viewModel.item))
// }
}
if canManageSubtitles {
// ChevronButton(L10n.subtitles) {
// router.route(to: \.editImages, ItemImagesViewModel(item: viewModel.item))
// }
}
}
// MARK: - Editable Metadata Components Routing Buttons
@ViewBuilder
private var editComponentsView: some View {
Section { Section {
ChevronButton(L10n.genres) { ChevronButton(L10n.genres) {
router.route(to: \.editGenres, viewModel.item) router.route(to: \.editGenres, viewModel.item)

View File

@ -30,33 +30,25 @@ struct ItemView: View {
@State @State
private var error: JellyfinAPIError? private var error: JellyfinAPIError?
@StoredValue(.User.enableItemDeletion) // MARK: - Can Delete Item
private var enableItemDeletion: Bool
@StoredValue(.User.enableItemEditing)
private var enableItemEditing: Bool
@StoredValue(.User.enableCollectionManagement)
private var enableCollectionManagement: Bool
private var canDelete: Bool { private var canDelete: Bool {
if viewModel.item.type == .boxSet { viewModel.userSession.user.permissions.items.canDelete(item: viewModel.item)
return enableCollectionManagement && viewModel.item.canDelete ?? false
} else {
return enableItemDeletion && viewModel.item.canDelete ?? false
}
} }
// MARK: - Can Edit Item
private var canEdit: Bool { private var canEdit: Bool {
if viewModel.item.type == .boxSet { viewModel.userSession.user.permissions.items.canEditMetadata(item: viewModel.item)
return enableCollectionManagement // TODO: Enable when Subtitle / Lyric Editing is added
} else { // || viewModel.userSession.user.permissions.items.canManageLyrics(item: viewModel.item)
return enableItemEditing // || viewModel.userSession.user.permissions.items.canManageSubtitles(item: viewModel.item)
}
} }
// Use to hide the menu button when not needed. // MARK: - Deletion or Editing is Enabled
// Add more checks as needed. For example, canDownload.
private var enableMenu: Bool { private var enableMenu: Bool {
canDelete || canEdit canEdit || canDelete
} }
private static func typeViewModel(for item: BaseItemDto) -> ItemViewModel { private static func typeViewModel(for item: BaseItemDto) -> ItemViewModel {
@ -149,9 +141,10 @@ struct ItemView: View {
} }
if canDelete { if canDelete {
Divider() Section {
Button(L10n.delete, systemImage: "trash", role: .destructive) { Button(L10n.delete, systemImage: "trash", role: .destructive) {
showConfirmationDialog = true showConfirmationDialog = true
}
} }
} }
} }

View File

@ -44,30 +44,21 @@ extension CustomizeViewsSettings {
selection: $enabledTrailers selection: $enabledTrailers
) )
/// Enable Editing Items from All Visible LIbraries /// Enabled Collection Management for collection managers
if userSession?.user.permissions.items.canEditMetadata ?? false { if userSession?.user.permissions.items.canManageCollections == true {
Toggle(L10n.editMedia, isOn: $enableItemEditing)
}
/// Enable Deleting Items from Approved Libraries
if userSession?.user.permissions.items.canDelete ?? false {
Toggle(L10n.deleteMedia, isOn: $enableItemDeletion)
}
/// Enable Downloading All Items
/* if userSession?.user.permissions.items.canDownload ?? false {
Toggle(L10n.itemDownloading, isOn: $enableItemDownloads)
} */
/// Enable Deleting or Editing Collections
if userSession?.user.permissions.items.canManageCollections ?? false {
Toggle(L10n.editCollections, isOn: $enableCollectionManagement) Toggle(L10n.editCollections, isOn: $enableCollectionManagement)
} }
/// Manage Item Lyrics /// Enabled Media Management when there are media elements that can be managed
/* if userSession?.user.permissions.items.canManageLyrics ?? false { if userSession?.user.permissions.items.canEditMetadata == true ||
Toggle(L10n.allowLyricsManagement isOn: $enableLyricsManagement) userSession?.user.permissions.items.canManageLyrics == true ||
} */ userSession?.user.permissions.items.canManageSubtitles == true
/// Manage Item Subtitles {
/* if userSession?.user.items.canManageSubtitles ?? false { Toggle(L10n.editMedia, isOn: $enableItemEditing)
Toggle(L10n.allowSubtitleManagement, isOn: $enableSubtitleManagement) }
} */ /// Enabled Media Deletion for valid deletion users
if userSession?.user.permissions.items.canDelete == true {
Toggle(L10n.deleteMedia, isOn: $enableItemDeletion)
}
} }
} }
} }