[iOS] Media Item Menu | Edit Metadata (#1323)
* Playback Quality - Learn More * TODO: Fix leading not working on second line. * Remove layoutDirection. * Implement for tvOS. Slightly different spacing. * VStack * WIP - tvOS Implementaiton. SUBJECT TO CHANGE / ELIMINATION. * Background Icon & formatting * wip * Review Changes. Remove unused Strings, clean up comments. * Remove duplicate items used for testing * Remove tvOS scrollIfLargerThanContainer for now. * Edit Text-based Metadata * ViewModel Cleanup * use binding extensions * Huge overhaul: - Fix the notification when metadata was updated to work with 100% consistency - Flip the locking to be true -> lock like server - Redo the whole itemEditorViewModel to be more in-line with other viewModels | also fixes iPad weirdness - Use itemViewModel for the edit view so I can just reuse those existing notifications instead of recreating the wheel - More human dates for people - Date of death instead of "End date" (yikes) * String fixes & overview size * Fix build issues & String cleanup * fix overview sizing, cleanup * itemMetadataWasEdited -> temMetadataDidChange * Creation of the NavigationBarMenuButtonModifier for an "ellipsis.circle" menu object in the toolbar. Makes it easier to ensure that this format looks the same throughout. * Custom vs Official Rating + Menu Button Label change * Menu button spacing and groundwork for other menu items (canDownload) since we already have the bool available. Currently disabled. * Linting --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
b9ac50c164
commit
da40f6a3b5
|
@ -80,8 +80,8 @@ final class ItemCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
func makeItemEditor(item: BaseItemDto) -> NavigationViewCoordinator<ItemEditorCoordinator> {
|
func makeItemEditor(viewModel: ItemViewModel) -> NavigationViewCoordinator<ItemEditorCoordinator> {
|
||||||
NavigationViewCoordinator(ItemEditorCoordinator(item: item))
|
NavigationViewCoordinator(ItemEditorCoordinator(viewModel: viewModel))
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeDownloadTask(downloadTask: DownloadTask) -> NavigationViewCoordinator<DownloadTaskCoordinator> {
|
func makeDownloadTask(downloadTask: DownloadTask) -> NavigationViewCoordinator<DownloadTaskCoordinator> {
|
||||||
|
|
|
@ -17,14 +17,23 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable {
|
||||||
@Root
|
@Root
|
||||||
var start = makeStart
|
var start = makeStart
|
||||||
|
|
||||||
private let item: BaseItemDto
|
private let viewModel: ItemViewModel
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
@Route(.modal)
|
||||||
self.item = item
|
var editMetadata = makeEditMetadata
|
||||||
|
|
||||||
|
init(viewModel: ItemViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeEditMetadata(item: BaseItemDto) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
|
NavigationViewCoordinator {
|
||||||
|
EditMetadataView(viewModel: ItemEditorViewModel(item: item))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeStart() -> some View {
|
func makeStart() -> some View {
|
||||||
ItemEditorView(item: item)
|
ItemEditorView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,3 +42,21 @@ extension Binding {
|
||||||
map(getter: { !$0 }, setter: { $0 })
|
map(getter: { !$0 }, setter: { $0 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Binding where Value: RangeReplaceableCollection, Value.Element: Equatable {
|
||||||
|
|
||||||
|
func contains(_ element: Value.Element) -> Binding<Bool> {
|
||||||
|
Binding<Bool>(
|
||||||
|
get: { wrappedValue.contains(element) },
|
||||||
|
set: { newValue in
|
||||||
|
if newValue {
|
||||||
|
if !wrappedValue.contains(element) {
|
||||||
|
wrappedValue.append(element)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wrappedValue.removeAll { $0 == element }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
// TODO: break into separate files
|
||||||
|
|
||||||
struct HourMinuteFormatStyle: FormatStyle {
|
struct HourMinuteFormatStyle: FormatStyle {
|
||||||
|
|
||||||
func format(_ value: TimeInterval) -> String {
|
func format(_ value: TimeInterval) -> String {
|
||||||
|
@ -78,6 +80,29 @@ extension ParseableFormatStyle where Self == DayIntervalParseableFormatStyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct NilIfEmptyStringFormatStyle: ParseableFormatStyle {
|
||||||
|
|
||||||
|
var parseStrategy: NilIfEmptyStringParseStrategy = .init()
|
||||||
|
|
||||||
|
func format(_ value: String?) -> String {
|
||||||
|
value ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NilIfEmptyStringParseStrategy: ParseStrategy {
|
||||||
|
|
||||||
|
func parse(_ value: String) -> String? {
|
||||||
|
value.isEmpty ? nil : value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ParseableFormatStyle where Self == NilIfEmptyStringFormatStyle {
|
||||||
|
|
||||||
|
static var nilIfEmptyString: NilIfEmptyStringFormatStyle {
|
||||||
|
.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension FormatStyle where Self == TimeIntervalFormatStyle {
|
extension FormatStyle where Self == TimeIntervalFormatStyle {
|
||||||
|
|
||||||
static func interval(
|
static func interval(
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// 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 MetadataField: Displayable {
|
||||||
|
var displayTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .cast:
|
||||||
|
return L10n.people
|
||||||
|
case .genres:
|
||||||
|
return L10n.genres
|
||||||
|
case .productionLocations:
|
||||||
|
return L10n.productionLocations
|
||||||
|
case .studios:
|
||||||
|
return L10n.studios
|
||||||
|
case .tags:
|
||||||
|
return L10n.tags
|
||||||
|
case .name:
|
||||||
|
return L10n.name
|
||||||
|
case .overview:
|
||||||
|
return L10n.overview
|
||||||
|
case .runtime:
|
||||||
|
return L10n.runtime
|
||||||
|
case .officialRating:
|
||||||
|
return L10n.officialRating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
//
|
||||||
|
// 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 Video3DFormat {
|
||||||
|
var displayTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .halfSideBySide:
|
||||||
|
return L10n.halfSideBySide
|
||||||
|
case .fullSideBySide:
|
||||||
|
return L10n.fullSideBySide
|
||||||
|
case .fullTopAndBottom:
|
||||||
|
return L10n.fullTopAndBottom
|
||||||
|
case .halfTopAndBottom:
|
||||||
|
return L10n.halfTopAndBottom
|
||||||
|
case .mvc:
|
||||||
|
return L10n.mvc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
enum BoxSetDisplayOrder: String, CaseIterable, Identifiable {
|
||||||
|
case dateModified = "DateModified"
|
||||||
|
case sortName = "SortName"
|
||||||
|
case premiereDate = "PremiereDate"
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .dateModified:
|
||||||
|
return L10n.dateModified
|
||||||
|
case .sortName:
|
||||||
|
return L10n.sortName
|
||||||
|
case .premiereDate:
|
||||||
|
return L10n.premiereDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
enum SeriesDisplayOrder: String, CaseIterable, Identifiable {
|
||||||
|
case aired = "Aired"
|
||||||
|
case originalAirDate
|
||||||
|
case absolute
|
||||||
|
case dvd
|
||||||
|
case digital
|
||||||
|
case storyArc
|
||||||
|
case production
|
||||||
|
case tv
|
||||||
|
case alternate
|
||||||
|
case regional
|
||||||
|
case alternateDVD = "altdvd"
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .aired:
|
||||||
|
return L10n.aired
|
||||||
|
case .originalAirDate:
|
||||||
|
return L10n.originalAirDate
|
||||||
|
case .absolute:
|
||||||
|
return L10n.absolute
|
||||||
|
case .dvd:
|
||||||
|
return L10n.dvd
|
||||||
|
case .digital:
|
||||||
|
return L10n.digital
|
||||||
|
case .storyArc:
|
||||||
|
return L10n.storyArc
|
||||||
|
case .production:
|
||||||
|
return L10n.production
|
||||||
|
case .tv:
|
||||||
|
return L10n.tv
|
||||||
|
case .alternate:
|
||||||
|
return L10n.alternate
|
||||||
|
case .regional:
|
||||||
|
return L10n.regional
|
||||||
|
case .alternateDVD:
|
||||||
|
return L10n.alternateDVD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
enum SeriesStatus: String, CaseIterable {
|
||||||
|
case continuing = "Continuing"
|
||||||
|
case ended = "Ended"
|
||||||
|
case unreleased = "Unreleased"
|
||||||
|
|
||||||
|
var displayTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .continuing:
|
||||||
|
return L10n.continuing
|
||||||
|
case .ended:
|
||||||
|
return L10n.ended
|
||||||
|
case .unreleased:
|
||||||
|
return L10n.unreleased
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,195 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
import OrderedCollections
|
||||||
|
|
||||||
|
class ItemEditorViewModel<ItemType: Equatable>: ViewModel, Stateful, Eventful {
|
||||||
|
|
||||||
|
// MARK: - Events
|
||||||
|
|
||||||
|
enum Event: Equatable {
|
||||||
|
case updated
|
||||||
|
case error(JellyfinAPIError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
enum Action: Equatable {
|
||||||
|
case add([ItemType])
|
||||||
|
case remove([ItemType])
|
||||||
|
case update(BaseItemDto)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: BackgroundState
|
||||||
|
|
||||||
|
enum BackgroundState: Hashable {
|
||||||
|
case refreshing
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - State
|
||||||
|
|
||||||
|
enum State: Hashable {
|
||||||
|
case initial
|
||||||
|
case error(JellyfinAPIError)
|
||||||
|
case updating
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var backgroundStates: OrderedSet<BackgroundState> = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var state: State = .initial
|
||||||
|
|
||||||
|
private var task: AnyCancellable?
|
||||||
|
private let eventSubject = PassthroughSubject<Event, Never>()
|
||||||
|
|
||||||
|
var events: AnyPublisher<Event, Never> {
|
||||||
|
eventSubject.receive(on: RunLoop.main).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
|
||||||
|
init(item: BaseItemDto) {
|
||||||
|
self.item = item
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Respond to Actions
|
||||||
|
|
||||||
|
func respond(to action: Action) -> State {
|
||||||
|
switch action {
|
||||||
|
case let .add(items):
|
||||||
|
task?.cancel()
|
||||||
|
|
||||||
|
task = Task { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
do {
|
||||||
|
await MainActor.run { self.state = .updating }
|
||||||
|
|
||||||
|
try await self.addComponents(items)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .initial
|
||||||
|
self.eventSubject.send(.updated)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
let apiError = JellyfinAPIError(error.localizedDescription)
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .error(apiError)
|
||||||
|
self.eventSubject.send(.error(apiError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.asAnyCancellable()
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
case let .remove(items):
|
||||||
|
task?.cancel()
|
||||||
|
|
||||||
|
task = Task { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
do {
|
||||||
|
await MainActor.run { self.state = .updating }
|
||||||
|
|
||||||
|
try await self.removeComponents(items)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .initial
|
||||||
|
self.eventSubject.send(.updated)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
let apiError = JellyfinAPIError(error.localizedDescription)
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .error(apiError)
|
||||||
|
self.eventSubject.send(.error(apiError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.asAnyCancellable()
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
case let .update(newItem):
|
||||||
|
task?.cancel()
|
||||||
|
|
||||||
|
task = Task { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
do {
|
||||||
|
await MainActor.run { self.state = .updating }
|
||||||
|
|
||||||
|
try await self.updateItem(newItem)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .initial
|
||||||
|
self.eventSubject.send(.updated)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
let apiError = JellyfinAPIError(error.localizedDescription)
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .error(apiError)
|
||||||
|
self.eventSubject.send(.error(apiError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.asAnyCancellable()
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Save Updated Item to Server
|
||||||
|
|
||||||
|
func updateItem(_ newItem: BaseItemDto, refresh: Bool = false) async throws {
|
||||||
|
guard let itemId = item.id else { return }
|
||||||
|
|
||||||
|
let request = Paths.updateItem(itemID: itemId, newItem)
|
||||||
|
_ = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
if refresh {
|
||||||
|
try await refreshItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
Notifications[.itemMetadataDidChange].post(object: newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Refresh Item
|
||||||
|
|
||||||
|
private func refreshItem() async throws {
|
||||||
|
guard let itemId = item.id else { return }
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
_ = backgroundStates.append(.refreshing)
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Paths.getItem(userID: userSession.user.id, itemID: itemId)
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.item = response.value
|
||||||
|
_ = backgroundStates.remove(.refreshing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Add Items (To be overridden)
|
||||||
|
|
||||||
|
func addComponents(_ items: [ItemType]) async throws {
|
||||||
|
fatalError("This method should be overridden in subclasses")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Remove Items (To be overridden)
|
||||||
|
|
||||||
|
func removeComponents(_ items: [ItemType]) async throws {
|
||||||
|
fatalError("This method should be overridden in subclasses")
|
||||||
|
}
|
||||||
|
}
|
|
@ -169,7 +169,7 @@ class RefreshMetadataViewModel: ViewModel, Stateful, Eventful {
|
||||||
self.item = response.value
|
self.item = response.value
|
||||||
self.progress = 0.0
|
self.progress = 0.0
|
||||||
|
|
||||||
Notifications[.itemMetadataDidChange].post(object: itemId)
|
Notifications[.itemMetadataDidChange].post(object: item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ItemViewModel: ViewModel, Stateful {
|
||||||
case backgroundRefresh
|
case backgroundRefresh
|
||||||
case error(JellyfinAPIError)
|
case error(JellyfinAPIError)
|
||||||
case refresh
|
case refresh
|
||||||
|
case replace(BaseItemDto)
|
||||||
case toggleIsFavorite
|
case toggleIsFavorite
|
||||||
case toggleIsPlayed
|
case toggleIsPlayed
|
||||||
}
|
}
|
||||||
|
@ -91,16 +92,19 @@ class ItemViewModel: ViewModel, Stateful {
|
||||||
// TODO: should replace with a more robust "PlaybackManager"
|
// TODO: should replace with a more robust "PlaybackManager"
|
||||||
Notifications[.itemMetadataDidChange].publisher
|
Notifications[.itemMetadataDidChange].publisher
|
||||||
.sink { [weak self] notification in
|
.sink { [weak self] notification in
|
||||||
|
if let userInfo = notification.object as? [String: String] {
|
||||||
guard let userInfo = notification.object as? [String: String] else { return }
|
if let itemID = userInfo["itemID"], itemID == item.id {
|
||||||
|
Task { [weak self] in
|
||||||
if let itemID = userInfo["itemID"], itemID == item.id {
|
await self?.send(.backgroundRefresh)
|
||||||
Task { [weak self] in
|
}
|
||||||
await self?.send(.backgroundRefresh)
|
} else if let seriesID = userInfo["seriesID"], seriesID == item.id {
|
||||||
|
Task { [weak self] in
|
||||||
|
await self?.send(.backgroundRefresh)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if let seriesID = userInfo["seriesID"], seriesID == item.id {
|
} else if let newItem = notification.object as? BaseItemDto, newItem.id == self?.item.id {
|
||||||
Task { [weak self] in
|
Task { [weak self] in
|
||||||
await self?.send(.backgroundRefresh)
|
await self?.send(.replace(newItem))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,6 +199,22 @@ class ItemViewModel: ViewModel, Stateful {
|
||||||
.asAnyCancellable()
|
.asAnyCancellable()
|
||||||
|
|
||||||
return .refreshing
|
return .refreshing
|
||||||
|
case let .replace(newItem):
|
||||||
|
|
||||||
|
backgroundStates.append(.refresh)
|
||||||
|
|
||||||
|
Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
do {
|
||||||
|
await MainActor.run {
|
||||||
|
self.backgroundStates.remove(.refresh)
|
||||||
|
self.item = newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
return state
|
||||||
case .toggleIsFavorite:
|
case .toggleIsFavorite:
|
||||||
|
|
||||||
toggleIsFavoriteTask?.cancel()
|
toggleIsFavoriteTask?.cancel()
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
final class ParentalRatingsViewModel: ViewModel, Stateful {
|
||||||
|
|
||||||
|
// MARK: Action
|
||||||
|
|
||||||
|
enum Action: Equatable {
|
||||||
|
case refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: State
|
||||||
|
|
||||||
|
enum State: Hashable {
|
||||||
|
case content
|
||||||
|
case error(JellyfinAPIError)
|
||||||
|
case initial
|
||||||
|
case refreshing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
private(set) var parentalRatings: [ParentalRating] = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
final var state: State = .initial
|
||||||
|
|
||||||
|
private var currentRefreshTask: AnyCancellable?
|
||||||
|
|
||||||
|
var hasNoResults: Bool {
|
||||||
|
parentalRatings.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func respond(to action: Action) -> State {
|
||||||
|
switch action {
|
||||||
|
case .refresh:
|
||||||
|
currentRefreshTask?.cancel()
|
||||||
|
|
||||||
|
currentRefreshTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let parentalRatings = try await getParentalRatings()
|
||||||
|
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.parentalRatings = parentalRatings
|
||||||
|
self.state = .content
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .error(.init(error.localizedDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.asAnyCancellable()
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Fetch Parental Ratings
|
||||||
|
|
||||||
|
private func getParentalRatings() async throws -> [ParentalRating] {
|
||||||
|
let request = Paths.getParentalRatings
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
return response.value
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,6 +79,35 @@
|
||||||
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 */; };
|
||||||
|
4E6619FC2CEFE2BE00025C99 /* 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 */; };
|
||||||
|
4E661A0F2CEFE46300025C99 /* SeriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0D2CEFE46300025C99 /* SeriesSection.swift */; };
|
||||||
|
4E661A102CEFE46300025C99 /* TitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0E2CEFE46300025C99 /* TitleSection.swift */; };
|
||||||
|
4E661A112CEFE46300025C99 /* LockMetadataSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A082CEFE46300025C99 /* LockMetadataSection.swift */; };
|
||||||
|
4E661A122CEFE46300025C99 /* MediaFormatSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A092CEFE46300025C99 /* MediaFormatSection.swift */; };
|
||||||
|
4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A062CEFE46300025C99 /* EpisodeSection.swift */; };
|
||||||
|
4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A052CEFE46300025C99 /* DisplayOrderSection.swift */; };
|
||||||
|
4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A072CEFE46300025C99 /* LocalizationSection.swift */; };
|
||||||
|
4E661A162CEFE46300025C99 /* ParentialRatingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0B2CEFE46300025C99 /* ParentialRatingsSection.swift */; };
|
||||||
|
4E661A172CEFE46300025C99 /* OverviewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0A2CEFE46300025C99 /* OverviewSection.swift */; };
|
||||||
|
4E661A182CEFE46300025C99 /* ReviewsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0C2CEFE46300025C99 /* ReviewsSection.swift */; };
|
||||||
|
4E661A192CEFE46300025C99 /* DateSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A042CEFE46300025C99 /* DateSection.swift */; };
|
||||||
|
4E661A1B2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A1A2CEFE53A00025C99 /* BoxSetDisplayOrder.swift */; };
|
||||||
|
4E661A1C2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A1A2CEFE53A00025C99 /* BoxSetDisplayOrder.swift */; };
|
||||||
|
4E661A1F2CEFE56E00025C99 /* SeriesDisplayOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A1E2CEFE56400025C99 /* SeriesDisplayOrder.swift */; };
|
||||||
|
4E661A202CEFE56E00025C99 /* SeriesDisplayOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A1E2CEFE56400025C99 /* SeriesDisplayOrder.swift */; };
|
||||||
|
4E661A222CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A212CEFE60C00025C99 /* ParentalRatingsViewModel.swift */; };
|
||||||
|
4E661A232CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A212CEFE60C00025C99 /* ParentalRatingsViewModel.swift */; };
|
||||||
|
4E661A252CEFE64500025C99 /* CountryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A242CEFE64200025C99 /* CountryPicker.swift */; };
|
||||||
|
4E661A272CEFE65000025C99 /* LanguagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A262CEFE64D00025C99 /* LanguagePicker.swift */; };
|
||||||
|
4E661A292CEFE68200025C99 /* Video3DFormatPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A282CEFE68100025C99 /* Video3DFormatPicker.swift */; };
|
||||||
|
4E661A2B2CEFE6F400025C99 /* Video3DFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A2A2CEFE6F300025C99 /* Video3DFormat.swift */; };
|
||||||
|
4E661A2C2CEFE6F400025C99 /* Video3DFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A2A2CEFE6F300025C99 /* Video3DFormat.swift */; };
|
||||||
|
4E661A2E2CEFE77700025C99 /* MetadataField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A2D2CEFE77700025C99 /* MetadataField.swift */; };
|
||||||
|
4E661A2F2CEFE77700025C99 /* MetadataField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A2D2CEFE77700025C99 /* MetadataField.swift */; };
|
||||||
|
4E661A312CEFE7BC00025C99 /* SeriesStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A302CEFE7B900025C99 /* SeriesStatus.swift */; };
|
||||||
|
4E661A322CEFE7BC00025C99 /* SeriesStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A302CEFE7B900025C99 /* SeriesStatus.swift */; };
|
||||||
4E699BB92CB33FC2007CBD5D /* HomeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E699BB82CB33FB5007CBD5D /* HomeSection.swift */; };
|
4E699BB92CB33FC2007CBD5D /* HomeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E699BB82CB33FB5007CBD5D /* HomeSection.swift */; };
|
||||||
4E699BC02CB3477D007CBD5D /* HomeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E699BBF2CB34775007CBD5D /* HomeSection.swift */; };
|
4E699BC02CB3477D007CBD5D /* HomeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E699BBF2CB34775007CBD5D /* HomeSection.swift */; };
|
||||||
4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */; };
|
4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */; };
|
||||||
|
@ -149,6 +178,7 @@
|
||||||
4EED874A2CBF824B002354D2 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87462CBF824B002354D2 /* DeviceRow.swift */; };
|
4EED874A2CBF824B002354D2 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87462CBF824B002354D2 /* DeviceRow.swift */; };
|
||||||
4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; };
|
4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; };
|
||||||
4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; };
|
4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; };
|
||||||
|
4EEEEA242CFA8E1500527D79 /* NavigationBarMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */; };
|
||||||
4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; };
|
4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; };
|
||||||
4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; };
|
4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; };
|
||||||
4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; };
|
4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; };
|
||||||
|
@ -1146,6 +1176,28 @@
|
||||||
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>"; };
|
||||||
|
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>"; };
|
||||||
|
4E661A042CEFE46300025C99 /* DateSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A052CEFE46300025C99 /* DisplayOrderSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayOrderSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A062CEFE46300025C99 /* EpisodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A072CEFE46300025C99 /* LocalizationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A082CEFE46300025C99 /* LockMetadataSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockMetadataSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A092CEFE46300025C99 /* MediaFormatSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFormatSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A0A2CEFE46300025C99 /* OverviewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A0B2CEFE46300025C99 /* ParentialRatingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentialRatingsSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A0C2CEFE46300025C99 /* ReviewsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A0D2CEFE46300025C99 /* SeriesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A0E2CEFE46300025C99 /* TitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleSection.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A1A2CEFE53A00025C99 /* BoxSetDisplayOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxSetDisplayOrder.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A1E2CEFE56400025C99 /* SeriesDisplayOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesDisplayOrder.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A212CEFE60C00025C99 /* ParentalRatingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentalRatingsViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A242CEFE64200025C99 /* CountryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryPicker.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A262CEFE64D00025C99 /* LanguagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePicker.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A282CEFE68100025C99 /* Video3DFormatPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video3DFormatPicker.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A2A2CEFE6F300025C99 /* Video3DFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video3DFormat.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A2D2CEFE77700025C99 /* MetadataField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataField.swift; sourceTree = "<group>"; };
|
||||||
|
4E661A302CEFE7B900025C99 /* SeriesStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesStatus.swift; sourceTree = "<group>"; };
|
||||||
4E699BB82CB33FB5007CBD5D /* HomeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSection.swift; sourceTree = "<group>"; };
|
4E699BB82CB33FB5007CBD5D /* HomeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSection.swift; sourceTree = "<group>"; };
|
||||||
4E699BBF2CB34775007CBD5D /* HomeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSection.swift; sourceTree = "<group>"; };
|
4E699BBF2CB34775007CBD5D /* HomeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSection.swift; sourceTree = "<group>"; };
|
||||||
4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionDetailView.swift; sourceTree = "<group>"; };
|
4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1205,6 +1257,7 @@
|
||||||
4EED87462CBF824B002354D2 /* DeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRow.swift; sourceTree = "<group>"; };
|
4EED87462CBF824B002354D2 /* DeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRow.swift; sourceTree = "<group>"; };
|
||||||
4EED87482CBF824B002354D2 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = "<group>"; };
|
4EED87482CBF824B002354D2 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = "<group>"; };
|
||||||
4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = "<group>"; };
|
4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarMenuButton.swift; sourceTree = "<group>"; };
|
||||||
4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = "<group>"; };
|
4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = "<group>"; };
|
||||||
4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = "<group>"; };
|
4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = "<group>"; };
|
||||||
4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
|
4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2123,6 +2176,50 @@
|
||||||
path = AdminDashboardView;
|
path = AdminDashboardView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4E6619FF2CEFE39000025C99 /* EditMetadataView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E661A022CEFE42200025C99 /* Components */,
|
||||||
|
4E661A002CEFE39900025C99 /* EditMetadataView.swift */,
|
||||||
|
);
|
||||||
|
path = EditMetadataView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4E661A022CEFE42200025C99 /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E661A032CEFE42800025C99 /* Sections */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4E661A032CEFE42800025C99 /* Sections */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E661A042CEFE46300025C99 /* DateSection.swift */,
|
||||||
|
4E661A052CEFE46300025C99 /* DisplayOrderSection.swift */,
|
||||||
|
4E661A062CEFE46300025C99 /* EpisodeSection.swift */,
|
||||||
|
4E661A072CEFE46300025C99 /* LocalizationSection.swift */,
|
||||||
|
4E661A082CEFE46300025C99 /* LockMetadataSection.swift */,
|
||||||
|
4E661A092CEFE46300025C99 /* MediaFormatSection.swift */,
|
||||||
|
4E661A0A2CEFE46300025C99 /* OverviewSection.swift */,
|
||||||
|
4E661A0B2CEFE46300025C99 /* ParentialRatingsSection.swift */,
|
||||||
|
4E661A0C2CEFE46300025C99 /* ReviewsSection.swift */,
|
||||||
|
4E661A0D2CEFE46300025C99 /* SeriesSection.swift */,
|
||||||
|
4E661A0E2CEFE46300025C99 /* TitleSection.swift */,
|
||||||
|
);
|
||||||
|
path = Sections;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4E661A1D2CEFE55200025C99 /* DisplayOrder */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E661A1A2CEFE53A00025C99 /* BoxSetDisplayOrder.swift */,
|
||||||
|
4E661A1E2CEFE56400025C99 /* SeriesDisplayOrder.swift */,
|
||||||
|
);
|
||||||
|
path = DisplayOrder;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4E699BB52CB33F4B007CBD5D /* CustomizeViewsSettings */ = {
|
4E699BB52CB33F4B007CBD5D /* CustomizeViewsSettings */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2188,6 +2285,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
4E8F74A62CE03D4C00CC8969 /* Components */,
|
4E8F74A62CE03D4C00CC8969 /* Components */,
|
||||||
|
4E6619FF2CEFE39000025C99 /* EditMetadataView */,
|
||||||
4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */,
|
4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */,
|
||||||
);
|
);
|
||||||
path = ItemEditorView;
|
path = ItemEditorView;
|
||||||
|
@ -2205,6 +2303,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */,
|
4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */,
|
||||||
|
4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */,
|
||||||
4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */,
|
4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = ItemEditorViewModel;
|
path = ItemEditorViewModel;
|
||||||
|
@ -2456,6 +2555,7 @@
|
||||||
E1EDA8D52B924CA500F9A57E /* LibraryViewModel */,
|
E1EDA8D52B924CA500F9A57E /* LibraryViewModel */,
|
||||||
C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */,
|
C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */,
|
||||||
E1CAF65C2BA345830087D991 /* MediaViewModel */,
|
E1CAF65C2BA345830087D991 /* MediaViewModel */,
|
||||||
|
4E661A212CEFE60C00025C99 /* ParentalRatingsViewModel.swift */,
|
||||||
E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */,
|
E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */,
|
||||||
6334175C287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift */,
|
6334175C287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift */,
|
||||||
E1BCDB4E2BE1F491009F6744 /* ResetUserPasswordViewModel.swift */,
|
E1BCDB4E2BE1F491009F6744 /* ResetUserPasswordViewModel.swift */,
|
||||||
|
@ -2564,6 +2664,7 @@
|
||||||
E1F5CF042CB09EA000607465 /* CurrentDate.swift */,
|
E1F5CF042CB09EA000607465 /* CurrentDate.swift */,
|
||||||
4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */,
|
4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */,
|
||||||
E17FB55128C119D400311DFE /* Displayable.swift */,
|
E17FB55128C119D400311DFE /* Displayable.swift */,
|
||||||
|
4E661A1D2CEFE55200025C99 /* DisplayOrder */,
|
||||||
E1579EA62B97DC1500A31CA1 /* Eventful.swift */,
|
E1579EA62B97DC1500A31CA1 /* Eventful.swift */,
|
||||||
E1092F4B29106F9F00163F57 /* GestureAction.swift */,
|
E1092F4B29106F9F00163F57 /* GestureAction.swift */,
|
||||||
E1D37F4A2B9CEA5C00343D2B /* ImageSource.swift */,
|
E1D37F4A2B9CEA5C00343D2B /* ImageSource.swift */,
|
||||||
|
@ -2584,6 +2685,7 @@
|
||||||
E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */,
|
E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */,
|
||||||
E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */,
|
E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */,
|
||||||
E164A7F52BE4814700A54B18 /* SelectUserServerSelection.swift */,
|
E164A7F52BE4814700A54B18 /* SelectUserServerSelection.swift */,
|
||||||
|
4E661A302CEFE7B900025C99 /* SeriesStatus.swift */,
|
||||||
E129429228F2845000796AC6 /* SliderType.swift */,
|
E129429228F2845000796AC6 /* SliderType.swift */,
|
||||||
E11042742B8013DF00821020 /* Stateful.swift */,
|
E11042742B8013DF00821020 /* Stateful.swift */,
|
||||||
E149CCAC2BE6ECC8008B9331 /* Storable.swift */,
|
E149CCAC2BE6ECC8008B9331 /* Storable.swift */,
|
||||||
|
@ -2842,6 +2944,7 @@
|
||||||
children = (
|
children = (
|
||||||
E1D8429429346C6400D1041A /* BasicStepper.swift */,
|
E1D8429429346C6400D1041A /* BasicStepper.swift */,
|
||||||
E133328C2953AE4B00EE76AB /* CircularProgressView.swift */,
|
E133328C2953AE4B00EE76AB /* CircularProgressView.swift */,
|
||||||
|
4E661A242CEFE64200025C99 /* CountryPicker.swift */,
|
||||||
E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */,
|
E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */,
|
||||||
E18E01A7288746AF0022598C /* DotHStack.swift */,
|
E18E01A7288746AF0022598C /* DotHStack.swift */,
|
||||||
E1DE2B492B97ECB900F6715F /* ErrorView.swift */,
|
E1DE2B492B97ECB900F6715F /* ErrorView.swift */,
|
||||||
|
@ -2849,6 +2952,7 @@
|
||||||
E178B0752BE435D70023651B /* HourMinutePicker.swift */,
|
E178B0752BE435D70023651B /* HourMinutePicker.swift */,
|
||||||
E1DC7AC92C63337C00AEE368 /* iOS15View.swift */,
|
E1DC7AC92C63337C00AEE368 /* iOS15View.swift */,
|
||||||
E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */,
|
E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */,
|
||||||
|
4E661A262CEFE64D00025C99 /* LanguagePicker.swift */,
|
||||||
4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */,
|
4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */,
|
||||||
4E16FD4E2C0183B500110147 /* LetterPickerBar */,
|
4E16FD4E2C0183B500110147 /* LetterPickerBar */,
|
||||||
E1A8FDEB2C0574A800D0A51C /* ListRow.swift */,
|
E1A8FDEB2C0574A800D0A51C /* ListRow.swift */,
|
||||||
|
@ -2866,6 +2970,7 @@
|
||||||
E1581E26291EF59800D6C640 /* SplitContentView.swift */,
|
E1581E26291EF59800D6C640 /* SplitContentView.swift */,
|
||||||
E1D27EE62BBC955F00152D16 /* UnmaskSecureField.swift */,
|
E1D27EE62BBC955F00152D16 /* UnmaskSecureField.swift */,
|
||||||
E157562F29355B7900976E1F /* UpdateView.swift */,
|
E157562F29355B7900976E1F /* UpdateView.swift */,
|
||||||
|
4E661A282CEFE68100025C99 /* Video3DFormatPicker.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3979,6 +4084,7 @@
|
||||||
4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */,
|
4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */,
|
||||||
E1F5F9B12BA0200500BA5014 /* MediaSourceInfo */,
|
E1F5F9B12BA0200500BA5014 /* MediaSourceInfo */,
|
||||||
E122A9122788EAAD0060FA63 /* MediaStream.swift */,
|
E122A9122788EAAD0060FA63 /* MediaStream.swift */,
|
||||||
|
4E661A2D2CEFE77700025C99 /* MetadataField.swift */,
|
||||||
E1AD105E26D9ADDD003E4A08 /* NameGuidPair.swift */,
|
E1AD105E26D9ADDD003E4A08 /* NameGuidPair.swift */,
|
||||||
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */,
|
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */,
|
||||||
4E2182E42CAF67EF0094806B /* PlayMethod.swift */,
|
4E2182E42CAF67EF0094806B /* PlayMethod.swift */,
|
||||||
|
@ -3994,6 +4100,7 @@
|
||||||
4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */,
|
4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */,
|
||||||
E1CB757B2C80F00D00217C76 /* TranscodingProfile.swift */,
|
E1CB757B2C80F00D00217C76 /* TranscodingProfile.swift */,
|
||||||
E18CE0B128A229E70092E7F1 /* UserDto.swift */,
|
E18CE0B128A229E70092E7F1 /* UserDto.swift */,
|
||||||
|
4E661A2A2CEFE6F300025C99 /* Video3DFormat.swift */,
|
||||||
);
|
);
|
||||||
path = JellyfinAPI;
|
path = JellyfinAPI;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -4031,6 +4138,7 @@
|
||||||
children = (
|
children = (
|
||||||
E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */,
|
E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */,
|
||||||
E1194F4D2BEABA9100888DB6 /* NavigationBarCloseButton.swift */,
|
E1194F4D2BEABA9100888DB6 /* NavigationBarCloseButton.swift */,
|
||||||
|
4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */,
|
||||||
E113133028BDB6D600930F75 /* NavigationBarDrawerButtons */,
|
E113133028BDB6D600930F75 /* NavigationBarDrawerButtons */,
|
||||||
E11895B12893842D0042947B /* NavigationBarOffset */,
|
E11895B12893842D0042947B /* NavigationBarOffset */,
|
||||||
);
|
);
|
||||||
|
@ -4700,6 +4808,7 @@
|
||||||
E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
|
E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
|
||||||
4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */,
|
4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */,
|
||||||
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */,
|
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */,
|
||||||
|
4E661A2B2CEFE6F400025C99 /* Video3DFormat.swift in Sources */,
|
||||||
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
||||||
53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */,
|
53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */,
|
||||||
E1575E99293E7B1E001665B1 /* UIColor.swift in Sources */,
|
E1575E99293E7B1E001665B1 /* UIColor.swift in Sources */,
|
||||||
|
@ -4730,6 +4839,7 @@
|
||||||
E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
||||||
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */,
|
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */,
|
||||||
E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */,
|
E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */,
|
||||||
|
4E6619FC2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */,
|
||||||
E1CB757F2C80F28F00217C76 /* SubtitleProfile.swift in Sources */,
|
E1CB757F2C80F28F00217C76 /* SubtitleProfile.swift in Sources */,
|
||||||
E1E0BEB829EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */,
|
E1E0BEB829EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */,
|
||||||
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
|
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
|
||||||
|
@ -4743,6 +4853,7 @@
|
||||||
E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */,
|
E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */,
|
||||||
E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */,
|
E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */,
|
||||||
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */,
|
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */,
|
||||||
|
4E661A2F2CEFE77700025C99 /* MetadataField.swift in Sources */,
|
||||||
E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */,
|
E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */,
|
||||||
E14E9DF22BCF7A99004E3371 /* ItemLetter.swift in Sources */,
|
E14E9DF22BCF7A99004E3371 /* ItemLetter.swift in Sources */,
|
||||||
E10B1EC82BD9AF6100A92EAF /* V2ServerModel.swift in Sources */,
|
E10B1EC82BD9AF6100A92EAF /* V2ServerModel.swift in Sources */,
|
||||||
|
@ -4786,6 +4897,7 @@
|
||||||
E1ED91162B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
|
E1ED91162B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
|
||||||
E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */,
|
E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */,
|
||||||
E1ED7FDB2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */,
|
E1ED7FDB2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */,
|
||||||
|
4E661A222CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */,
|
||||||
E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */,
|
E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */,
|
||||||
E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */,
|
E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */,
|
||||||
E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */,
|
E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */,
|
||||||
|
@ -4883,6 +4995,7 @@
|
||||||
E1575E75293E77B5001665B1 /* LibraryDisplayType.swift in Sources */,
|
E1575E75293E77B5001665B1 /* LibraryDisplayType.swift in Sources */,
|
||||||
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
|
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
|
||||||
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
|
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
|
||||||
|
4E661A312CEFE7BC00025C99 /* SeriesStatus.swift in Sources */,
|
||||||
E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */,
|
E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */,
|
||||||
E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */,
|
E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */,
|
||||||
E1575E69293E77B5001665B1 /* ItemSortBy.swift in Sources */,
|
E1575E69293E77B5001665B1 /* ItemSortBy.swift in Sources */,
|
||||||
|
@ -4948,6 +5061,7 @@
|
||||||
E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */,
|
E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */,
|
||||||
4E2AC4CC2C6C494E00DD600D /* VideoCodec.swift in Sources */,
|
4E2AC4CC2C6C494E00DD600D /* VideoCodec.swift in Sources */,
|
||||||
4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */,
|
4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */,
|
||||||
|
4E661A1B2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */,
|
||||||
E129428628F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */,
|
E129428628F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */,
|
||||||
53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */,
|
53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */,
|
||||||
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||||
|
@ -4955,6 +5069,7 @@
|
||||||
C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */,
|
C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */,
|
||||||
E1575E9F293E7B1E001665B1 /* Int.swift in Sources */,
|
E1575E9F293E7B1E001665B1 /* Int.swift in Sources */,
|
||||||
E1D9F475296E86D400129AF3 /* NativeVideoPlayer.swift in Sources */,
|
E1D9F475296E86D400129AF3 /* NativeVideoPlayer.swift in Sources */,
|
||||||
|
4E661A1F2CEFE56E00025C99 /* SeriesDisplayOrder.swift in Sources */,
|
||||||
E145EB462BE0AD4E003BF6F3 /* Set.swift in Sources */,
|
E145EB462BE0AD4E003BF6F3 /* Set.swift in Sources */,
|
||||||
E1575E7D293E77B5001665B1 /* PosterDisplayType.swift in Sources */,
|
E1575E7D293E77B5001665B1 /* PosterDisplayType.swift in Sources */,
|
||||||
E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */,
|
E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */,
|
||||||
|
@ -5104,6 +5219,7 @@
|
||||||
4EC2B1A22CC96F6600D866BE /* ServerUsersViewModel.swift in Sources */,
|
4EC2B1A22CC96F6600D866BE /* ServerUsersViewModel.swift in Sources */,
|
||||||
E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */,
|
E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */,
|
||||||
E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */,
|
E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */,
|
||||||
|
4E661A292CEFE68200025C99 /* Video3DFormatPicker.swift in Sources */,
|
||||||
E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */,
|
E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */,
|
||||||
621338932660107500A81A2A /* String.swift in Sources */,
|
621338932660107500A81A2A /* String.swift in Sources */,
|
||||||
E17AC96F2954EE4B003D2BC2 /* DownloadListViewModel.swift in Sources */,
|
E17AC96F2954EE4B003D2BC2 /* DownloadListViewModel.swift in Sources */,
|
||||||
|
@ -5125,6 +5241,7 @@
|
||||||
62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */,
|
62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */,
|
||||||
E1A1528A28FD22F600600579 /* TextPairView.swift in Sources */,
|
E1A1528A28FD22F600600579 /* TextPairView.swift in Sources */,
|
||||||
E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */,
|
E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */,
|
||||||
|
4E661A232CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */,
|
||||||
E170D0E2294CC8000017224C /* VideoPlayer+Actions.swift in Sources */,
|
E170D0E2294CC8000017224C /* VideoPlayer+Actions.swift in Sources */,
|
||||||
4E10C8112CC030CD0012CC9F /* DeviceDetailsView.swift in Sources */,
|
4E10C8112CC030CD0012CC9F /* DeviceDetailsView.swift in Sources */,
|
||||||
E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
||||||
|
@ -5176,6 +5293,17 @@
|
||||||
E15D63ED2BD622A700AA665D /* CompactChannelView.swift in Sources */,
|
E15D63ED2BD622A700AA665D /* CompactChannelView.swift in Sources */,
|
||||||
E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */,
|
E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */,
|
||||||
E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */,
|
E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */,
|
||||||
|
4E661A0F2CEFE46300025C99 /* SeriesSection.swift in Sources */,
|
||||||
|
4E661A102CEFE46300025C99 /* TitleSection.swift in Sources */,
|
||||||
|
4E661A112CEFE46300025C99 /* LockMetadataSection.swift in Sources */,
|
||||||
|
4E661A122CEFE46300025C99 /* MediaFormatSection.swift in Sources */,
|
||||||
|
4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */,
|
||||||
|
4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */,
|
||||||
|
4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */,
|
||||||
|
4E661A162CEFE46300025C99 /* ParentialRatingsSection.swift in Sources */,
|
||||||
|
4E661A172CEFE46300025C99 /* OverviewSection.swift in Sources */,
|
||||||
|
4E661A182CEFE46300025C99 /* ReviewsSection.swift in Sources */,
|
||||||
|
4E661A192CEFE46300025C99 /* DateSection.swift in Sources */,
|
||||||
E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */,
|
E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */,
|
||||||
E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
||||||
E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */,
|
E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */,
|
||||||
|
@ -5184,8 +5312,10 @@
|
||||||
4EB538C52CE3E25700EB72D5 /* ExternalAccessSection.swift in Sources */,
|
4EB538C52CE3E25700EB72D5 /* ExternalAccessSection.swift in Sources */,
|
||||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||||
E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */,
|
E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */,
|
||||||
|
4E661A1C2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */,
|
||||||
E133328829538D8D00EE76AB /* Files.swift in Sources */,
|
E133328829538D8D00EE76AB /* Files.swift in Sources */,
|
||||||
C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */,
|
C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */,
|
||||||
|
4E661A322CEFE7BC00025C99 /* SeriesStatus.swift in Sources */,
|
||||||
E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */,
|
E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */,
|
||||||
BD39577E2C1140810078CEF8 /* TransitionSection.swift in Sources */,
|
BD39577E2C1140810078CEF8 /* TransitionSection.swift in Sources */,
|
||||||
4EC2B1A52CC96FA400D866BE /* ServerUserAdminViewModel.swift in Sources */,
|
4EC2B1A52CC96FA400D866BE /* ServerUserAdminViewModel.swift in Sources */,
|
||||||
|
@ -5205,6 +5335,7 @@
|
||||||
E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
||||||
4E2AC4C82C6C493C00DD600D /* SubtitleFormat.swift in Sources */,
|
4E2AC4C82C6C493C00DD600D /* SubtitleFormat.swift in Sources */,
|
||||||
E19D41B02BF2B7540082B8B2 /* URLSessionConfiguration.swift in Sources */,
|
E19D41B02BF2B7540082B8B2 /* URLSessionConfiguration.swift in Sources */,
|
||||||
|
4E661A2E2CEFE77700025C99 /* MetadataField.swift in Sources */,
|
||||||
E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */,
|
E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */,
|
||||||
4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */,
|
4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */,
|
||||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||||
|
@ -5319,6 +5450,7 @@
|
||||||
E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */,
|
E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */,
|
||||||
4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */,
|
4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */,
|
||||||
E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
|
E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
|
||||||
|
4EEEEA242CFA8E1500527D79 /* NavigationBarMenuButton.swift in Sources */,
|
||||||
4EC2B1A92CC97C0700D866BE /* ServerUserDetailsView.swift in Sources */,
|
4EC2B1A92CC97C0700D866BE /* ServerUserDetailsView.swift in Sources */,
|
||||||
E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */,
|
E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */,
|
||||||
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */,
|
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */,
|
||||||
|
@ -5333,6 +5465,7 @@
|
||||||
E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */,
|
E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */,
|
||||||
E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */,
|
E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */,
|
||||||
E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */,
|
E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */,
|
||||||
|
4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */,
|
||||||
C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */,
|
C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */,
|
||||||
E18E01AD288746AF0022598C /* DotHStack.swift in Sources */,
|
E18E01AD288746AF0022598C /* DotHStack.swift in Sources */,
|
||||||
E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */,
|
E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */,
|
||||||
|
@ -5350,6 +5483,7 @@
|
||||||
E152107C2947ACA000375CC2 /* InvertedLightAppIcon.swift in Sources */,
|
E152107C2947ACA000375CC2 /* InvertedLightAppIcon.swift in Sources */,
|
||||||
E17AC9732955007A003D2BC2 /* DownloadTaskButton.swift in Sources */,
|
E17AC9732955007A003D2BC2 /* DownloadTaskButton.swift in Sources */,
|
||||||
E145EB4F2BE168AC003BF6F3 /* SwiftfinStore+ServerState.swift in Sources */,
|
E145EB4F2BE168AC003BF6F3 /* SwiftfinStore+ServerState.swift in Sources */,
|
||||||
|
4E661A2C2CEFE6F400025C99 /* Video3DFormat.swift in Sources */,
|
||||||
E1A1528228FD126C00600579 /* VerticalAlignment.swift in Sources */,
|
E1A1528228FD126C00600579 /* VerticalAlignment.swift in Sources */,
|
||||||
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
|
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
|
||||||
E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */,
|
E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */,
|
||||||
|
@ -5425,10 +5559,12 @@
|
||||||
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||||
4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */,
|
4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */,
|
||||||
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||||
|
4E6619FD2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */,
|
||||||
E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */,
|
E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */,
|
||||||
E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */,
|
E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */,
|
||||||
E10231392BCF8A3C009D71FC /* ProgramButtonContent.swift in Sources */,
|
E10231392BCF8A3C009D71FC /* ProgramButtonContent.swift in Sources */,
|
||||||
E1DC9844296DECB600982F06 /* ProgressIndicator.swift in Sources */,
|
E1DC9844296DECB600982F06 /* ProgressIndicator.swift in Sources */,
|
||||||
|
4E661A202CEFE56E00025C99 /* SeriesDisplayOrder.swift in Sources */,
|
||||||
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||||
4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */,
|
4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */,
|
||||||
E10B1ECA2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */,
|
E10B1ECA2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */,
|
||||||
|
@ -5439,6 +5575,7 @@
|
||||||
4E026A8B2CE804E7005471B5 /* ResetUserPasswordView.swift in Sources */,
|
4E026A8B2CE804E7005471B5 /* ResetUserPasswordView.swift in Sources */,
|
||||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||||
|
4E661A252CEFE64500025C99 /* CountryPicker.swift in Sources */,
|
||||||
E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */,
|
E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */,
|
||||||
4E10C8192CC045700012CC9F /* CustomDeviceNameSection.swift in Sources */,
|
4E10C8192CC045700012CC9F /* CustomDeviceNameSection.swift in Sources */,
|
||||||
4EB538BD2CE3CCD100EB72D5 /* MediaPlaybackSection.swift in Sources */,
|
4EB538BD2CE3CCD100EB72D5 /* MediaPlaybackSection.swift in Sources */,
|
||||||
|
@ -5561,6 +5698,7 @@
|
||||||
E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */,
|
E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */,
|
||||||
E19D41AC2BF288110082B8B2 /* ServerCheckView.swift in Sources */,
|
E19D41AC2BF288110082B8B2 /* ServerCheckView.swift in Sources */,
|
||||||
E1D5C39928DF914700CDBEFB /* CapsuleSlider.swift in Sources */,
|
E1D5C39928DF914700CDBEFB /* CapsuleSlider.swift in Sources */,
|
||||||
|
4E661A272CEFE65000025C99 /* LanguagePicker.swift in Sources */,
|
||||||
62E1DCC3273CE19800C9AE76 /* URL.swift in Sources */,
|
62E1DCC3273CE19800C9AE76 /* URL.swift in Sources */,
|
||||||
E11BDF7A2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */,
|
E11BDF7A2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */,
|
||||||
E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */,
|
E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
struct CountryPicker: View {
|
||||||
|
let title: String
|
||||||
|
@Binding
|
||||||
|
var selectedCountryCode: String?
|
||||||
|
|
||||||
|
// MARK: - Get all localized countries
|
||||||
|
|
||||||
|
private var countries: [(code: String?, name: String)] {
|
||||||
|
var uniqueCountries = Set<String>()
|
||||||
|
|
||||||
|
var countryList: [(code: String?, name: String)] = Locale.isoRegionCodes.compactMap { code in
|
||||||
|
let locale = Locale.current
|
||||||
|
if let name = locale.localizedString(forRegionCode: code),
|
||||||
|
!uniqueCountries.contains(code)
|
||||||
|
{
|
||||||
|
uniqueCountries.insert(code)
|
||||||
|
return (code, name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
.sorted { $0.name < $1.name }
|
||||||
|
|
||||||
|
// Add None as an option at the top of the list
|
||||||
|
countryList.insert((code: nil, name: L10n.none), at: 0)
|
||||||
|
return countryList
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Picker(title, selection: $selectedCountryCode) {
|
||||||
|
ForEach(countries, id: \.code) { country in
|
||||||
|
Text(country.name).tag(country.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
struct LanguagePicker: View {
|
||||||
|
let title: String
|
||||||
|
@Binding
|
||||||
|
var selectedLanguageCode: String?
|
||||||
|
|
||||||
|
// MARK: - Get all localized languages
|
||||||
|
|
||||||
|
private var languages: [(code: String?, name: String)] {
|
||||||
|
var uniqueLanguages = Set<String>()
|
||||||
|
|
||||||
|
var languageList: [(code: String?, name: String)] = Locale.availableIdentifiers.compactMap { identifier in
|
||||||
|
let locale = Locale(identifier: identifier)
|
||||||
|
if let code = locale.languageCode,
|
||||||
|
let name = locale.localizedString(forLanguageCode: code),
|
||||||
|
!uniqueLanguages.contains(code)
|
||||||
|
{
|
||||||
|
uniqueLanguages.insert(code)
|
||||||
|
return (code, name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
.sorted { $0.name < $1.name }
|
||||||
|
|
||||||
|
// Add None as an option at the top of the list
|
||||||
|
languageList.insert((code: nil, name: L10n.none), at: 0)
|
||||||
|
return languageList
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Picker(title, selection: $selectedLanguageCode) {
|
||||||
|
ForEach(languages, id: \.code) { language in
|
||||||
|
Text(language.name).tag(language.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// 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 JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct Video3DFormatPicker: View {
|
||||||
|
let title: String
|
||||||
|
@Binding
|
||||||
|
var selectedFormat: Video3DFormat?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Picker(title, selection: $selectedFormat) {
|
||||||
|
Text(L10n.none).tag(nil as Video3DFormat?)
|
||||||
|
ForEach(Video3DFormat.allCases, id: \.self) { format in
|
||||||
|
Text(format.displayTitle).tag(format as Video3DFormat?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
struct NavigationBarMenuButtonModifier<Content: View>: ViewModifier {
|
||||||
|
|
||||||
|
@Default(.accentColor)
|
||||||
|
private var accentColor
|
||||||
|
|
||||||
|
let isLoading: Bool
|
||||||
|
let isHidden: Bool
|
||||||
|
let items: () -> Content
|
||||||
|
|
||||||
|
func body(content: Self.Content) -> some View {
|
||||||
|
content.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||||
|
|
||||||
|
if isLoading {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isHidden {
|
||||||
|
Menu(L10n.options, systemImage: "ellipsis.circle") {
|
||||||
|
items()
|
||||||
|
}
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.backport
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,6 +79,22 @@ extension View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func navigationBarMenuButton<Content: View>(
|
||||||
|
isLoading: Bool = false,
|
||||||
|
isHidden: Bool = false,
|
||||||
|
@ViewBuilder
|
||||||
|
_ items: @escaping () -> Content
|
||||||
|
) -> some View {
|
||||||
|
modifier(
|
||||||
|
NavigationBarMenuButtonModifier(
|
||||||
|
isLoading: isLoading,
|
||||||
|
isHidden: isHidden,
|
||||||
|
items: items
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func listRowCornerRadius(_ radius: CGFloat) -> some View {
|
func listRowCornerRadius(_ radius: CGFloat) -> some View {
|
||||||
if #unavailable(iOS 16) {
|
if #unavailable(iOS 16) {
|
||||||
|
|
|
@ -61,8 +61,20 @@ struct ServerUsersView: View {
|
||||||
navigationBarSelectView
|
navigationBarSelectView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
navigationBarEditView
|
if isEditing {
|
||||||
|
Button(isEditing ? L10n.cancel : L10n.edit) {
|
||||||
|
isEditing.toggle()
|
||||||
|
|
||||||
|
UIDevice.impact(.light)
|
||||||
|
|
||||||
|
if !isEditing {
|
||||||
|
selectedUsers.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.foregroundStyle(accentColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ToolbarItem(placement: .bottomBar) {
|
ToolbarItem(placement: .bottomBar) {
|
||||||
if isEditing {
|
if isEditing {
|
||||||
|
@ -75,6 +87,28 @@ struct ServerUsersView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationBarMenuButton(
|
||||||
|
isLoading: viewModel.backgroundStates.contains(.gettingUsers),
|
||||||
|
isHidden: isEditing
|
||||||
|
) {
|
||||||
|
Button(L10n.addUser, systemImage: "plus") {
|
||||||
|
router.route(to: \.addServerUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.users.isNotEmpty {
|
||||||
|
Button(L10n.editUsers, systemImage: "checkmark.circle") {
|
||||||
|
isEditing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Section(L10n.filters) {
|
||||||
|
Toggle(L10n.hidden, systemImage: "eye.slash", isOn: $isHiddenFilterActive)
|
||||||
|
Toggle(L10n.disabled, systemImage: "person.slash", isOn: $isDisabledFilterActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.onChange(of: isDisabledFilterActive) { newValue in
|
.onChange(of: isDisabledFilterActive) { newValue in
|
||||||
viewModel.send(.getUsers(
|
viewModel.send(.getUsers(
|
||||||
isHidden: isHiddenFilterActive,
|
isHidden: isHiddenFilterActive,
|
||||||
|
@ -175,51 +209,6 @@ struct ServerUsersView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Navigation Bar Edit Content
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var navigationBarEditView: some View {
|
|
||||||
if viewModel.backgroundStates.contains(.gettingUsers) {
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
|
|
||||||
if isEditing {
|
|
||||||
Button(isEditing ? L10n.cancel : L10n.edit) {
|
|
||||||
isEditing.toggle()
|
|
||||||
|
|
||||||
UIDevice.impact(.light)
|
|
||||||
|
|
||||||
if !isEditing {
|
|
||||||
selectedUsers.removeAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.toolbarPill)
|
|
||||||
.foregroundStyle(accentColor)
|
|
||||||
} else {
|
|
||||||
Menu(L10n.options, systemImage: "ellipsis.circle") {
|
|
||||||
Button(L10n.addUser, systemImage: "plus") {
|
|
||||||
router.route(to: \.addServerUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.users.isNotEmpty {
|
|
||||||
Button(L10n.editUsers, systemImage: "checkmark.circle") {
|
|
||||||
isEditing = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
Section(L10n.filters) {
|
|
||||||
Toggle(L10n.hidden, systemImage: "eye.slash", isOn: $isHiddenFilterActive)
|
|
||||||
Toggle(L10n.disabled, systemImage: "person.slash", isOn: $isDisabledFilterActive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.backport
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Navigation Bar Select/Remove All Content
|
// MARK: - Navigation Bar Select/Remove All Content
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct DateSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
let itemType: BaseItemKind
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.dates) {
|
||||||
|
DatePicker(
|
||||||
|
L10n.dateAdded,
|
||||||
|
selection: $item.dateCreated.coalesce(.now),
|
||||||
|
displayedComponents: .date
|
||||||
|
)
|
||||||
|
|
||||||
|
DatePicker(
|
||||||
|
itemType == .person ? L10n.birthday : L10n.releaseDate,
|
||||||
|
selection: $item.premiereDate.coalesce(.now),
|
||||||
|
displayedComponents: .date
|
||||||
|
)
|
||||||
|
|
||||||
|
if itemType == .series || itemType == .person {
|
||||||
|
DatePicker(
|
||||||
|
itemType == .person ? L10n.dateOfDeath : L10n.endDate,
|
||||||
|
selection: $item.endDate.coalesce(.now),
|
||||||
|
displayedComponents: .date
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(L10n.year) {
|
||||||
|
TextField(
|
||||||
|
itemType == .person ? L10n.birthYear : L10n.year,
|
||||||
|
value: $item.productionYear,
|
||||||
|
format: .number.grouping(.never)
|
||||||
|
)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct DisplayOrderSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
let itemType: BaseItemKind
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.displayOrder) {
|
||||||
|
switch itemType {
|
||||||
|
case .boxSet:
|
||||||
|
Picker(
|
||||||
|
L10n.displayOrder,
|
||||||
|
selection: $item.displayOrder
|
||||||
|
.coalesce("")
|
||||||
|
.map(
|
||||||
|
getter: { BoxSetDisplayOrder(rawValue: $0) ?? .dateModified },
|
||||||
|
setter: { $0.rawValue }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ForEach(BoxSetDisplayOrder.allCases) { order in
|
||||||
|
Text(order.displayTitle).tag(order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .series:
|
||||||
|
Picker(
|
||||||
|
L10n.displayOrder,
|
||||||
|
selection: $item.displayOrder
|
||||||
|
.coalesce("")
|
||||||
|
.map(
|
||||||
|
getter: { SeriesDisplayOrder(rawValue: $0) ?? .aired },
|
||||||
|
setter: { $0.rawValue }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ForEach(SeriesDisplayOrder.allCases) { order in
|
||||||
|
Text(order.displayTitle).tag(order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct EpisodeSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.season) {
|
||||||
|
|
||||||
|
// MARK: Season Number
|
||||||
|
|
||||||
|
ChevronAlertButton(
|
||||||
|
L10n.season,
|
||||||
|
subtitle: item.parentIndexNumber?.description,
|
||||||
|
description: L10n.enterSeasonNumber
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
L10n.season,
|
||||||
|
value: $item.parentIndexNumber,
|
||||||
|
format: .number
|
||||||
|
)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Episode Number
|
||||||
|
|
||||||
|
ChevronAlertButton(
|
||||||
|
L10n.episode,
|
||||||
|
subtitle: item.indexNumber?.description,
|
||||||
|
description: L10n.enterEpisodeNumber
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
L10n.episode,
|
||||||
|
value: $item.indexNumber,
|
||||||
|
format: .number
|
||||||
|
)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct LocalizationSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.metadataPreferences) {
|
||||||
|
LanguagePicker(
|
||||||
|
title: L10n.language,
|
||||||
|
selectedLanguageCode: $item.preferredMetadataLanguage
|
||||||
|
)
|
||||||
|
|
||||||
|
CountryPicker(
|
||||||
|
title: L10n.country,
|
||||||
|
selectedCountryCode: $item.preferredMetadataCountryCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct LockMetadataSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
// TODO: Animation when lockAllFields is selected
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.lockedFields) {
|
||||||
|
Toggle(
|
||||||
|
L10n.lockAllFields,
|
||||||
|
isOn: $item.lockData.coalesce(false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.lockData != true {
|
||||||
|
Section {
|
||||||
|
ForEach(MetadataField.allCases, id: \.self) { field in
|
||||||
|
Toggle(
|
||||||
|
field.displayTitle,
|
||||||
|
isOn: $item.lockedFields
|
||||||
|
.coalesce([])
|
||||||
|
.contains(field)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct MediaFormatSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.format) {
|
||||||
|
TextField(
|
||||||
|
L10n.originalAspectRatio,
|
||||||
|
value: $item.aspectRatio,
|
||||||
|
format: .nilIfEmptyString
|
||||||
|
)
|
||||||
|
|
||||||
|
Video3DFormatPicker(
|
||||||
|
title: L10n.format3D,
|
||||||
|
selectedFormat: $item.video3DFormat
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct OverviewSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
let itemType: BaseItemKind
|
||||||
|
|
||||||
|
private var showTaglines: Bool {
|
||||||
|
[
|
||||||
|
BaseItemKind.movie,
|
||||||
|
.series,
|
||||||
|
.audioBook,
|
||||||
|
.book,
|
||||||
|
.audio,
|
||||||
|
].contains(itemType)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if showTaglines {
|
||||||
|
// There doesn't seem to be a usage anywhere of more than 1 tagline?
|
||||||
|
Section(L10n.taglines) {
|
||||||
|
TextField(
|
||||||
|
L10n.tagline,
|
||||||
|
value: $item.taglines
|
||||||
|
.map(
|
||||||
|
getter: { $0 == nil ? "" : $0!.first },
|
||||||
|
setter: { $0 == nil ? [] : [$0!] }
|
||||||
|
),
|
||||||
|
format: .nilIfEmptyString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(L10n.overview) {
|
||||||
|
TextEditor(text: $item.overview.coalesce(""))
|
||||||
|
.onAppear {
|
||||||
|
// Workaround for iOS 17 and earlier bug
|
||||||
|
// where the row height won't be set properly
|
||||||
|
item.overview = item.overview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// TODO: Reimagine this whole thing to be much leaner.
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct ParentalRatingSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
private var viewModel = ParentalRatingsViewModel()
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var officialRatings: [ParentalRating] = []
|
||||||
|
@State
|
||||||
|
private var customRatings: [ParentalRating] = []
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.parentalRating) {
|
||||||
|
|
||||||
|
// MARK: Official Rating Picker
|
||||||
|
|
||||||
|
Picker(
|
||||||
|
L10n.officialRating,
|
||||||
|
selection: $item.officialRating
|
||||||
|
.map(
|
||||||
|
getter: { value in officialRatings.first { $0.name == value } },
|
||||||
|
setter: { $0?.name }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(L10n.none).tag(nil as ParentalRating?)
|
||||||
|
ForEach(officialRatings, id: \.self) { rating in
|
||||||
|
Text(rating.name ?? "").tag(rating as ParentalRating?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
updateOfficialRatings()
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.parentalRatings) { _ in
|
||||||
|
updateOfficialRatings()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Custom Rating Picker
|
||||||
|
|
||||||
|
Picker(
|
||||||
|
L10n.customRating,
|
||||||
|
selection: $item.customRating
|
||||||
|
.map(
|
||||||
|
getter: { value in customRatings.first { $0.name == value } },
|
||||||
|
setter: { $0?.name }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(L10n.none).tag(nil as ParentalRating?)
|
||||||
|
ForEach(customRatings, id: \.self) { rating in
|
||||||
|
Text(rating.name ?? "").tag(rating as ParentalRating?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
updateCustomRatings()
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.parentalRatings) { _ in
|
||||||
|
updateCustomRatings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFirstAppear {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Update Official Rating
|
||||||
|
|
||||||
|
private func updateOfficialRatings() {
|
||||||
|
officialRatings = viewModel.parentalRatings
|
||||||
|
if let currentRatingName = item.officialRating,
|
||||||
|
!officialRatings.contains(where: { $0.name == currentRatingName })
|
||||||
|
{
|
||||||
|
officialRatings.append(ParentalRating(name: currentRatingName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Update Custom Rating
|
||||||
|
|
||||||
|
private func updateCustomRatings() {
|
||||||
|
customRatings = viewModel.parentalRatings
|
||||||
|
if let currentRatingName = item.customRating,
|
||||||
|
!customRatings.contains(where: { $0.name == currentRatingName })
|
||||||
|
{
|
||||||
|
customRatings.append(ParentalRating(name: currentRatingName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct ReviewsSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.reviews) {
|
||||||
|
|
||||||
|
// MARK: Critics Rating
|
||||||
|
|
||||||
|
ChevronAlertButton(
|
||||||
|
L10n.critics,
|
||||||
|
subtitle: item.criticRating.map { "\($0)" } ?? .emptyDash,
|
||||||
|
description: L10n.ratingDescription(L10n.critics)
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
L10n.rating,
|
||||||
|
value: $item.criticRating,
|
||||||
|
format: .number.precision(.fractionLength(1))
|
||||||
|
)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.onChange(of: item.criticRating) { _ in
|
||||||
|
if let rating = item.criticRating {
|
||||||
|
item.criticRating = min(max(rating, 0), 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Community Rating
|
||||||
|
|
||||||
|
ChevronAlertButton(
|
||||||
|
L10n.community,
|
||||||
|
subtitle: item.communityRating.map { "\($0)" } ?? .emptyDash,
|
||||||
|
description: L10n.ratingDescription(L10n.community)
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
L10n.rating,
|
||||||
|
value: $item.communityRating,
|
||||||
|
format: .number.precision(.fractionLength(1))
|
||||||
|
)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.onChange(of: item.communityRating) { _ in
|
||||||
|
if let rating = item.communityRating {
|
||||||
|
item.communityRating = min(max(rating, 0), 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct SeriesSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
private var item: BaseItemDto
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var tempRunTime: Int?
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init(item: Binding<BaseItemDto>) {
|
||||||
|
self._item = item
|
||||||
|
self.tempRunTime = Int(ServerTicks(item.wrappedValue.runTimeTicks ?? 0).minutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
|
||||||
|
Section(L10n.series) {
|
||||||
|
seriesStatusView
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(L10n.episodes) {
|
||||||
|
airTimeView
|
||||||
|
|
||||||
|
runTimeView
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(L10n.dayOfWeek) {
|
||||||
|
airDaysView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Series Status View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var seriesStatusView: some View {
|
||||||
|
Picker(
|
||||||
|
L10n.status,
|
||||||
|
selection: $item.status
|
||||||
|
.coalesce("")
|
||||||
|
.map(
|
||||||
|
getter: { SeriesStatus(rawValue: $0) ?? .continuing },
|
||||||
|
setter: { $0.rawValue }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ForEach(SeriesStatus.allCases, id: \.self) { status in
|
||||||
|
Text(status.displayTitle).tag(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Air Time View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var airTimeView: some View {
|
||||||
|
DatePicker(
|
||||||
|
L10n.airTime,
|
||||||
|
selection: $item.airTime
|
||||||
|
.coalesce("00:00")
|
||||||
|
.map(
|
||||||
|
getter: { parseAirTimeToDate($0) },
|
||||||
|
setter: { formatDateToString($0) }
|
||||||
|
),
|
||||||
|
displayedComponents: .hourAndMinute
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Air Days View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var airDaysView: some View {
|
||||||
|
ForEach(DayOfWeek.allCases, id: \.self) { field in
|
||||||
|
Toggle(
|
||||||
|
field.displayTitle ?? L10n.unknown,
|
||||||
|
isOn: $item.airDays
|
||||||
|
.coalesce([])
|
||||||
|
.contains(field)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Run Time View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var runTimeView: some View {
|
||||||
|
ChevronAlertButton(
|
||||||
|
L10n.runTime,
|
||||||
|
subtitle: ServerTicks(item.runTimeTicks ?? 0)
|
||||||
|
.seconds.formatted(.hourMinute),
|
||||||
|
description: L10n.episodeRuntimeDescription
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
L10n.minutes,
|
||||||
|
value: $tempRunTime
|
||||||
|
.coalesce(0)
|
||||||
|
.min(0),
|
||||||
|
format: .number
|
||||||
|
)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
} onSave: {
|
||||||
|
if let tempRunTime, tempRunTime != 0 {
|
||||||
|
item.runTimeTicks = ServerTicks(minutes: tempRunTime).ticks
|
||||||
|
} else {
|
||||||
|
item.runTimeTicks = nil
|
||||||
|
}
|
||||||
|
} onCancel: {
|
||||||
|
if let originalRunTime = item.runTimeTicks {
|
||||||
|
tempRunTime = Int(ServerTicks(originalRunTime).minutes)
|
||||||
|
} else {
|
||||||
|
tempRunTime = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Parse AirTime to Date
|
||||||
|
|
||||||
|
private func parseAirTimeToDate(_ airTime: String) -> Date {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "HH:mm"
|
||||||
|
return dateFormatter.date(from: airTime) ?? Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Format Date to String
|
||||||
|
|
||||||
|
private func formatDateToString(_ date: Date) -> String {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "HH:mm"
|
||||||
|
return dateFormatter.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditMetadataView {
|
||||||
|
|
||||||
|
struct TitleSection: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.title) {
|
||||||
|
TextField(
|
||||||
|
L10n.title,
|
||||||
|
value: $item.name,
|
||||||
|
format: .nilIfEmptyString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(L10n.originalTitle) {
|
||||||
|
TextField(
|
||||||
|
L10n.originalTitle,
|
||||||
|
value: $item.originalTitle,
|
||||||
|
format: .nilIfEmptyString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(L10n.sortTitle) {
|
||||||
|
TextField(
|
||||||
|
L10n.sortTitle,
|
||||||
|
value: $item.forcedSortName,
|
||||||
|
format: .nilIfEmptyString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
//
|
||||||
|
// 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 Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EditMetadataView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
private var viewModel: ItemEditorViewModel<BaseItemDto>
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var tempItem: BaseItemDto
|
||||||
|
|
||||||
|
private let itemType: BaseItemKind
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init(viewModel: ItemEditorViewModel<BaseItemDto>) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self._item = Binding(get: { viewModel.item }, set: { viewModel.item = $0 })
|
||||||
|
self._tempItem = State(initialValue: viewModel.item)
|
||||||
|
self.itemType = viewModel.item.type!
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var body: some View {
|
||||||
|
contentView
|
||||||
|
.navigationBarTitle(L10n.metadata)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.topBarTrailing {
|
||||||
|
Button(L10n.save) {
|
||||||
|
item = tempItem
|
||||||
|
viewModel.send(.update(tempItem))
|
||||||
|
router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.disabled(viewModel.item == tempItem)
|
||||||
|
}
|
||||||
|
.navigationBarCloseButton {
|
||||||
|
router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Content View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var contentView: some View {
|
||||||
|
Form {
|
||||||
|
TitleSection(item: $tempItem)
|
||||||
|
|
||||||
|
DateSection(
|
||||||
|
item: $tempItem,
|
||||||
|
itemType: itemType
|
||||||
|
)
|
||||||
|
|
||||||
|
if itemType == .series {
|
||||||
|
SeriesSection(item: $tempItem)
|
||||||
|
} else if itemType == .episode {
|
||||||
|
EpisodeSection(item: $tempItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
OverviewSection(
|
||||||
|
item: $tempItem,
|
||||||
|
itemType: itemType
|
||||||
|
)
|
||||||
|
|
||||||
|
ReviewsSection(item: $tempItem)
|
||||||
|
|
||||||
|
ParentalRatingSection(item: $tempItem)
|
||||||
|
|
||||||
|
if [BaseItemKind.movie, .episode].contains(itemType) {
|
||||||
|
MediaFormatSection(item: $tempItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalizationSection(item: $tempItem)
|
||||||
|
|
||||||
|
LockMetadataSection(item: $tempItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,8 +18,8 @@ struct ItemEditorView: View {
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: ItemEditorCoordinator.Router
|
private var router: ItemEditorCoordinator.Router
|
||||||
|
|
||||||
@State
|
@ObservedObject
|
||||||
var item: BaseItemDto
|
var viewModel: ItemViewModel
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
|
|
||||||
|
@ -30,10 +30,6 @@ struct ItemEditorView: View {
|
||||||
.navigationBarCloseButton {
|
.navigationBarCloseButton {
|
||||||
router.dismissCoordinator()
|
router.dismissCoordinator()
|
||||||
}
|
}
|
||||||
.onNotification(.itemMetadataDidChange) { notification in
|
|
||||||
guard let newItem = notification.object as? BaseItemDto else { return }
|
|
||||||
item = newItem
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Content View
|
// MARK: - Content View
|
||||||
|
@ -41,33 +37,50 @@ struct ItemEditorView: View {
|
||||||
private var contentView: some View {
|
private var contentView: some View {
|
||||||
List {
|
List {
|
||||||
ListTitleSection(
|
ListTitleSection(
|
||||||
item.name ?? L10n.unknown,
|
viewModel.item.name ?? L10n.unknown,
|
||||||
description: item.path
|
description: viewModel.item.path
|
||||||
)
|
)
|
||||||
|
|
||||||
Section {
|
refreshButtonView
|
||||||
RefreshMetadataButton(item: item)
|
|
||||||
.environment(\.isEnabled, userSession?.user.isAdministrator ?? false)
|
editView
|
||||||
} footer: {
|
}
|
||||||
LearnMoreButton(L10n.metadata) {
|
}
|
||||||
TextPair(
|
|
||||||
title: L10n.findMissing,
|
@ViewBuilder
|
||||||
subtitle: L10n.findMissingDescription
|
private var refreshButtonView: some View {
|
||||||
)
|
Section {
|
||||||
TextPair(
|
RefreshMetadataButton(item: viewModel.item)
|
||||||
title: L10n.replaceMetadata,
|
.environment(\.isEnabled, userSession?.user.isAdministrator ?? false)
|
||||||
subtitle: L10n.replaceMetadataDescription
|
} footer: {
|
||||||
)
|
LearnMoreButton(L10n.metadata) {
|
||||||
TextPair(
|
TextPair(
|
||||||
title: L10n.replaceImages,
|
title: L10n.findMissing,
|
||||||
subtitle: L10n.replaceImagesDescription
|
subtitle: L10n.findMissingDescription
|
||||||
)
|
)
|
||||||
TextPair(
|
TextPair(
|
||||||
title: L10n.replaceAll,
|
title: L10n.replaceMetadata,
|
||||||
subtitle: L10n.replaceAllDescription
|
subtitle: L10n.replaceMetadataDescription
|
||||||
)
|
)
|
||||||
}
|
TextPair(
|
||||||
|
title: L10n.replaceImages,
|
||||||
|
subtitle: L10n.replaceImagesDescription
|
||||||
|
)
|
||||||
|
TextPair(
|
||||||
|
title: L10n.replaceAll,
|
||||||
|
subtitle: L10n.replaceAllDescription
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var editView: some View {
|
||||||
|
Section(L10n.edit) {
|
||||||
|
ChevronButton(L10n.metadata)
|
||||||
|
.onSelect {
|
||||||
|
router.route(to: \.editMetadata, viewModel.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,12 @@ struct ItemView: View {
|
||||||
enableItemDeletion && viewModel.item.canDelete ?? false
|
enableItemDeletion && viewModel.item.canDelete ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
// As more menu items exist, this can either be expanded to include more validation or removed if there are permanent menu items.
|
private var canDownload: Bool {
|
||||||
|
viewModel.item.canDownload ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use to hide the menu button when not needed.
|
||||||
|
// Add more checks as needed. For example, canDownload.
|
||||||
private var enableMenu: Bool {
|
private var enableMenu: Bool {
|
||||||
canDelete || enableItemEditor
|
canDelete || enableItemEditor
|
||||||
}
|
}
|
||||||
|
@ -123,12 +128,21 @@ struct ItemView: View {
|
||||||
.onFirstAppear {
|
.onFirstAppear {
|
||||||
viewModel.send(.refresh)
|
viewModel.send(.refresh)
|
||||||
}
|
}
|
||||||
.topBarTrailing {
|
.navigationBarMenuButton(
|
||||||
if viewModel.backgroundStates.contains(.refresh) {
|
isLoading: viewModel.backgroundStates.contains(.refresh),
|
||||||
ProgressView()
|
isHidden: !enableMenu
|
||||||
|
) {
|
||||||
|
if enableItemEditor {
|
||||||
|
Button(L10n.edit, systemImage: "pencil") {
|
||||||
|
router.route(to: \.itemEditor, viewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if enableMenu {
|
|
||||||
itemActionMenu
|
if canDelete {
|
||||||
|
Divider()
|
||||||
|
Button(L10n.delete, systemImage: "trash", role: .destructive) {
|
||||||
|
showConfirmationDialog = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
|
@ -159,27 +173,4 @@ struct ItemView: View {
|
||||||
Text(error.localizedDescription)
|
Text(error.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var itemActionMenu: some View {
|
|
||||||
|
|
||||||
Menu(L10n.options, systemImage: "ellipsis.circle") {
|
|
||||||
|
|
||||||
if enableItemEditor {
|
|
||||||
Button(L10n.edit, systemImage: "pencil") {
|
|
||||||
router.route(to: \.itemEditor, viewModel.item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if canDelete {
|
|
||||||
Divider()
|
|
||||||
Button(L10n.delete, systemImage: "trash", role: .destructive) {
|
|
||||||
showConfirmationDialog = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.backport
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -471,35 +471,27 @@ struct PagingLibraryView<Element: Poster>: View {
|
||||||
viewModel.send(.refresh)
|
viewModel.send(.refresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.topBarTrailing {
|
.navigationBarMenuButton(
|
||||||
|
isLoading: viewModel.backgroundStates.contains(.gettingNextPage)
|
||||||
if viewModel.backgroundStates.contains(.gettingNextPage) {
|
) {
|
||||||
ProgressView()
|
if Defaults[.Customization.Library.rememberLayout] {
|
||||||
|
LibraryViewTypeToggle(
|
||||||
|
posterType: $posterType,
|
||||||
|
viewType: $displayType,
|
||||||
|
listColumnCount: $listColumnCount
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LibraryViewTypeToggle(
|
||||||
|
posterType: $defaultPosterType,
|
||||||
|
viewType: $defaultDisplayType,
|
||||||
|
listColumnCount: $defaultListColumnCount
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
Button(L10n.random, systemImage: "dice.fill") {
|
||||||
|
viewModel.send(.getRandomItem)
|
||||||
if Defaults[.Customization.Library.rememberLayout] {
|
|
||||||
LibraryViewTypeToggle(
|
|
||||||
posterType: $posterType,
|
|
||||||
viewType: $displayType,
|
|
||||||
listColumnCount: $listColumnCount
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LibraryViewTypeToggle(
|
|
||||||
posterType: $defaultPosterType,
|
|
||||||
viewType: $defaultDisplayType,
|
|
||||||
listColumnCount: $defaultListColumnCount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(L10n.random, systemImage: "dice.fill") {
|
|
||||||
viewModel.send(.getRandomItem)
|
|
||||||
}
|
|
||||||
.disabled(viewModel.elements.isEmpty)
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "ellipsis.circle")
|
|
||||||
}
|
}
|
||||||
|
.disabled(viewModel.elements.isEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue