Skip to content

Commit

Permalink
[iOS] Media Item Menu - Edit Arrays (People, Genres, Studios, & Tags) (
Browse files Browse the repository at this point in the history
…#1336)

* Cleanup / Genre & Tag Management

* Move searching to a backgroundState. Fix the font Color when bulk editing tags / genres should be secondary when editing & not selected

* Cleanup

* Now that cancelling is handled better this should prevent the issue where the suggestions fails to update on a letter entry

* Change from using an event for searchResults to using a published searchResults var

* Moved all logic to a local list where all genres/tags are populated on refresh then filterd locally instead of calling the server for changes.

* Inheritance

* Split metadata from components then alphabetize. Also, fix but where you can't add a people

* People & Permissions

* Functional but dirty. TODO: Cleanup + Trie? Trei?

* nil coalescing operator is only evaluated if the lhs is nil, coalescing operator with nil as rhs is redundant

* TODO: Search improvements & Delay search on name change

* Cleanup & reordering

* Debouncing

* Trie implementation

* Permissions Cleanup Squeezing in: jellyfin/jellyfin-web#6361

* enhance Trie

* cleanup

* cleanup

---------

Co-authored-by: Ethan Pippin <[email protected]>
  • Loading branch information
JPKribs and LePips authored Dec 6, 2024
1 parent 95c4395 commit a3d84a9
Show file tree
Hide file tree
Showing 30 changed files with 2,063 additions and 237 deletions.
112 changes: 112 additions & 0 deletions Shared/Coordinators/ItemEditorCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,131 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable {

private let viewModel: ItemViewModel

// MARK: - Route to Metadata

@Route(.modal)
var editMetadata = makeEditMetadata

// MARK: - Route to Genres

@Route(.push)
var editGenres = makeEditGenres
@Route(.modal)
var addGenre = makeAddGenre

// MARK: - Route to Tags

@Route(.push)
var editTags = makeEditTags
@Route(.modal)
var addTag = makeAddTag

// MARK: - Route to Studios

@Route(.push)
var editStudios = makeEditStudios
@Route(.modal)
var addStudio = makeAddStudio

// MARK: - Route to People

@Route(.push)
var editPeople = makeEditPeople
@Route(.modal)
var addPeople = makeAddPeople

// MARK: - Initializer

init(viewModel: ItemViewModel) {
self.viewModel = viewModel
}

// MARK: - Item Metadata

func makeEditMetadata(item: BaseItemDto) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
EditMetadataView(viewModel: ItemEditorViewModel(item: item))
}
}

// MARK: - Item Genres

@ViewBuilder
func makeEditGenres(item: BaseItemDto) -> some View {
EditItemElementView<String>(
viewModel: GenreEditorViewModel(item: item),
type: .genres,
route: { router, viewModel in
router.route(to: \.addGenre, viewModel as! GenreEditorViewModel)
}
)
}

func makeAddGenre(viewModel: GenreEditorViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
AddItemElementView(viewModel: viewModel, type: .genres)
}
}

// MARK: - Item Tags

@ViewBuilder
func makeEditTags(item: BaseItemDto) -> some View {
EditItemElementView<String>(
viewModel: TagEditorViewModel(item: item),
type: .tags,
route: { router, viewModel in
router.route(to: \.addTag, viewModel as! TagEditorViewModel)
}
)
}

func makeAddTag(viewModel: TagEditorViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
AddItemElementView(viewModel: viewModel, type: .tags)
}
}

// MARK: - Item Studios

@ViewBuilder
func makeEditStudios(item: BaseItemDto) -> some View {
EditItemElementView<NameGuidPair>(
viewModel: StudioEditorViewModel(item: item),
type: .studios,
route: { router, viewModel in
router.route(to: \.addStudio, viewModel as! StudioEditorViewModel)
}
)
}

func makeAddStudio(viewModel: StudioEditorViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
AddItemElementView(viewModel: viewModel, type: .studios)
}
}

// MARK: - Item People

@ViewBuilder
func makeEditPeople(item: BaseItemDto) -> some View {
EditItemElementView<BaseItemPerson>(
viewModel: PeopleEditorViewModel(item: item),
type: .people,
route: { router, viewModel in
router.route(to: \.addPeople, viewModel as! PeopleEditorViewModel)
}
)
}

func makeAddPeople(viewModel: PeopleEditorViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
AddItemElementView(viewModel: viewModel, type: .people)
}
}

// MARK: - Start

@ViewBuilder
func makeStart() -> some View {
ItemEditorView(viewModel: viewModel)
Expand Down
4 changes: 4 additions & 0 deletions Shared/Extensions/Collection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ extension Collection {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}

func keyed<Key>(using: KeyPath<Element, Key>) -> [Key: Element] {
Dictionary(uniqueKeysWithValues: map { ($0[keyPath: using], $0) })
}
}
97 changes: 97 additions & 0 deletions Shared/Extensions/JellyfinAPI/PersonKind.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// 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

// TODO: No longer needed in 10.9+
public enum PersonKind: String, Codable, CaseIterable {
case unknown = "Unknown"
case actor = "Actor"
case director = "Director"
case composer = "Composer"
case writer = "Writer"
case guestStar = "GuestStar"
case producer = "Producer"
case conductor = "Conductor"
case lyricist = "Lyricist"
case arranger = "Arranger"
case engineer = "Engineer"
case mixer = "Mixer"
case remixer = "Remixer"
case creator = "Creator"
case artist = "Artist"
case albumArtist = "AlbumArtist"
case author = "Author"
case illustrator = "Illustrator"
case penciller = "Penciller"
case inker = "Inker"
case colorist = "Colorist"
case letterer = "Letterer"
case coverArtist = "CoverArtist"
case editor = "Editor"
case translator = "Translator"
}

// TODO: Still needed in 10.9+
extension PersonKind: Displayable {
var displayTitle: String {
switch self {
case .unknown:
return L10n.unknown
case .actor:
return L10n.actor
case .director:
return L10n.director
case .composer:
return L10n.composer
case .writer:
return L10n.writer
case .guestStar:
return L10n.guestStar
case .producer:
return L10n.producer
case .conductor:
return L10n.conductor
case .lyricist:
return L10n.lyricist
case .arranger:
return L10n.arranger
case .engineer:
return L10n.engineer
case .mixer:
return L10n.mixer
case .remixer:
return L10n.remixer
case .creator:
return L10n.creator
case .artist:
return L10n.artist
case .albumArtist:
return L10n.albumArtist
case .author:
return L10n.author
case .illustrator:
return L10n.illustrator
case .penciller:
return L10n.penciller
case .inker:
return L10n.inker
case .colorist:
return L10n.colorist
case .letterer:
return L10n.letterer
case .coverArtist:
return L10n.coverArtist
case .editor:
return L10n.editor
case .translator:
return L10n.translator
}
}
}
112 changes: 112 additions & 0 deletions Shared/Objects/ItemArrayElements.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// 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
import SwiftUI

enum ItemArrayElements: Displayable {
case studios
case genres
case tags
case people

// MARK: - Localized Title

var displayTitle: String {
switch self {
case .studios:
return L10n.studios
case .genres:
return L10n.genres
case .tags:
return L10n.tags
case .people:
return L10n.people
}
}

// MARK: - Localized Description

var description: String {
switch self {
case .studios:
return L10n.studiosDescription
case .genres:
return L10n.genresDescription
case .tags:
return L10n.tagsDescription
case .people:
return L10n.peopleDescription
}
}

// MARK: - Create Element from Components

func createElement<T: Hashable>(
name: String,
id: String?,
personRole: String?,
personKind: String?
) -> T {
switch self {
case .genres, .tags:
return name as! T
case .studios:
return NameGuidPair(id: id, name: name) as! T
case .people:
return BaseItemPerson(
id: id,
name: name,
role: personRole,
type: personKind
) as! T
}
}

// MARK: - Get the Element from the BaseItemDto Based on Type

func getElement<T: Hashable>(for item: BaseItemDto) -> [T] {
switch self {
case .studios:
return item.studios as? [T] ?? []
case .genres:
return item.genres as? [T] ?? []
case .tags:
return item.tags as? [T] ?? []
case .people:
return item.people as? [T] ?? []
}
}

// MARK: - Get the Name from the Element Based on Type

func getId(for element: AnyHashable) -> String? {
switch self {
case .genres, .tags:
return nil
case .studios:
return (element.base as? NameGuidPair)?.id
case .people:
return (element.base as? BaseItemPerson)?.id
}
}

// MARK: - Get the Id from the Element Based on Type

func getName(for element: AnyHashable) -> String {
switch self {
case .genres, .tags:
return element.base as? String ?? L10n.unknown
case .studios:
return (element.base as? NameGuidPair)?.name ?? L10n.unknown
case .people:
return (element.base as? BaseItemPerson)?.name ?? L10n.unknown
}
}
}
4 changes: 2 additions & 2 deletions Shared/Objects/LibraryDisplayType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ enum LibraryDisplayType: String, CaseIterable, Displayable, Storable, SystemImag
var displayTitle: String {
switch self {
case .grid:
"Grid"
L10n.grid
case .list:
"List"
L10n.list
}
}

Expand Down
Loading

0 comments on commit a3d84a9

Please sign in to comment.