[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:
parent
8194057d40
commit
a3dab2e165
|
@ -29,3 +29,10 @@ extension BaseItemKind: ItemFilter {
|
||||||
rawValue
|
rawValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension BaseItemKind {
|
||||||
|
|
||||||
|
static var itemIdentifiableCases: [BaseItemKind] {
|
||||||
|
[.boxSet, .movie, .person, .series]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue