[iOS] Admin Dashboard - User Activity (#1485)
* Very Very WIP * Details page. TODOs: - Duplicate ViewModels are initialized. - Routing Cleanup - Localizations for fields - Get Played Item Details (See ActiveSessionDetails) - Move all details to ActivityDetailsViewModel for Users & Items - Localizations for enums - Enum the types if possible * Details View complete. TODO: - Filters - Default with No Filters * Ready * Fix localization * cleanup --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
715ddca793
commit
d4330f130b
|
@ -17,6 +17,13 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
@Root
|
@Root
|
||||||
var start = makeStart
|
var start = makeStart
|
||||||
|
|
||||||
|
// MARK: - Route: User Activity
|
||||||
|
|
||||||
|
@Route(.push)
|
||||||
|
var activity = makeActivityLogs
|
||||||
|
@Route(.push)
|
||||||
|
var activityDetails = makeActivityDetails
|
||||||
|
|
||||||
// MARK: - Route: Active Sessions
|
// MARK: - Route: Active Sessions
|
||||||
|
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
|
@ -84,6 +91,18 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var apiKeys = makeAPIKeys
|
var apiKeys = makeAPIKeys
|
||||||
|
|
||||||
|
// MARK: - Views: User Activity
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeActivityLogs() -> some View {
|
||||||
|
ServerActivityView()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeActivityDetails(viewModel: ServerActivityDetailViewModel) -> some View {
|
||||||
|
ServerActivityDetailsView(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Views: Active Sessions
|
// MARK: - Views: Active Sessions
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension ActivityLogEntry: Poster {
|
||||||
|
var displayTitle: String {
|
||||||
|
name ?? L10n.unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
var unwrappedIDHashOrZero: Int {
|
||||||
|
id?.hashValue ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemImage: String {
|
||||||
|
"text.document"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
//
|
||||||
|
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension LogLevel: SystemImageable, Displayable {
|
||||||
|
public var color: Color {
|
||||||
|
switch self {
|
||||||
|
case .trace:
|
||||||
|
return .gray.opacity(0.7)
|
||||||
|
case .debug:
|
||||||
|
return .gray
|
||||||
|
case .information:
|
||||||
|
return .blue
|
||||||
|
case .warning:
|
||||||
|
return .orange
|
||||||
|
case .error:
|
||||||
|
return .red
|
||||||
|
case .critical:
|
||||||
|
return .purple
|
||||||
|
case .none:
|
||||||
|
return .secondary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var systemImage: String {
|
||||||
|
switch self {
|
||||||
|
case .trace:
|
||||||
|
return "ant"
|
||||||
|
case .debug:
|
||||||
|
return "ladybug"
|
||||||
|
case .information:
|
||||||
|
return "info.circle"
|
||||||
|
case .warning:
|
||||||
|
return "exclamationmark.triangle"
|
||||||
|
case .error:
|
||||||
|
return "exclamationmark.circle"
|
||||||
|
case .critical:
|
||||||
|
return "xmark.octagon"
|
||||||
|
case .none:
|
||||||
|
return "questionmark.circle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var displayTitle: String {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,10 @@ extension Sequence {
|
||||||
filter { $0[keyPath: keyPath] != nil }
|
filter { $0[keyPath: keyPath] != nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func first<V: Equatable>(property: (Element) -> V, equalTo value: V) -> Element? {
|
||||||
|
first { property($0) == value }
|
||||||
|
}
|
||||||
|
|
||||||
func intersection<Value: Equatable>(_ other: some Sequence<Value>, using keyPath: KeyPath<Element, Value>) -> [Element] {
|
func intersection<Value: Equatable>(_ other: some Sequence<Value>, using keyPath: KeyPath<Element, Value>) -> [Element] {
|
||||||
filter { other.contains($0[keyPath: keyPath]) }
|
filter { other.contains($0[keyPath: keyPath]) }
|
||||||
}
|
}
|
||||||
|
@ -58,6 +62,10 @@ extension Sequence {
|
||||||
|
|
||||||
extension Sequence where Element: Equatable {
|
extension Sequence where Element: Equatable {
|
||||||
|
|
||||||
|
func first(equalTo other: Element) -> Element? {
|
||||||
|
first { $0 == other }
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an array containing the elements of the sequence that
|
/// Returns an array containing the elements of the sequence that
|
||||||
/// are also within the given sequence.
|
/// are also within the given sequence.
|
||||||
func intersection(_ other: some Sequence<Element>) -> [Element] {
|
func intersection(_ other: some Sequence<Element>) -> [Element] {
|
||||||
|
|
|
@ -40,6 +40,8 @@ internal enum L10n {
|
||||||
internal static let active = L10n.tr("Localizable", "active", fallback: "Active")
|
internal static let active = L10n.tr("Localizable", "active", fallback: "Active")
|
||||||
/// Activity
|
/// Activity
|
||||||
internal static let activity = L10n.tr("Localizable", "activity", fallback: "Activity")
|
internal static let activity = L10n.tr("Localizable", "activity", fallback: "Activity")
|
||||||
|
/// Activity log
|
||||||
|
internal static let activityLog = L10n.tr("Localizable", "activityLog", fallback: "Activity log")
|
||||||
/// Actor
|
/// Actor
|
||||||
internal static let actor = L10n.tr("Localizable", "actor", fallback: "Actor")
|
internal static let actor = L10n.tr("Localizable", "actor", fallback: "Actor")
|
||||||
/// Add
|
/// Add
|
||||||
|
@ -392,6 +394,8 @@ internal enum L10n {
|
||||||
internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard")
|
internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard")
|
||||||
/// Perform administrative tasks for your Jellyfin server.
|
/// Perform administrative tasks for your Jellyfin server.
|
||||||
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.")
|
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.")
|
||||||
|
/// Date
|
||||||
|
internal static let date = L10n.tr("Localizable", "date", fallback: "Date")
|
||||||
/// Date Added
|
/// Date Added
|
||||||
internal static let dateAdded = L10n.tr("Localizable", "dateAdded", fallback: "Date Added")
|
internal static let dateAdded = L10n.tr("Localizable", "dateAdded", fallback: "Date Added")
|
||||||
/// Date created
|
/// Date created
|
||||||
|
@ -760,6 +764,8 @@ internal enum L10n {
|
||||||
internal static let letterer = L10n.tr("Localizable", "letterer", fallback: "Letterer")
|
internal static let letterer = L10n.tr("Localizable", "letterer", fallback: "Letterer")
|
||||||
/// Letter Picker
|
/// Letter Picker
|
||||||
internal static let letterPicker = L10n.tr("Localizable", "letterPicker", fallback: "Letter Picker")
|
internal static let letterPicker = L10n.tr("Localizable", "letterPicker", fallback: "Letter Picker")
|
||||||
|
/// Level
|
||||||
|
internal static let level = L10n.tr("Localizable", "level", fallback: "Level")
|
||||||
/// Library
|
/// Library
|
||||||
internal static let library = L10n.tr("Localizable", "library", fallback: "Library")
|
internal static let library = L10n.tr("Localizable", "library", fallback: "Library")
|
||||||
/// Light
|
/// Light
|
||||||
|
@ -1130,6 +1136,8 @@ internal enum L10n {
|
||||||
internal static let reset = L10n.tr("Localizable", "reset", fallback: "Reset")
|
internal static let reset = L10n.tr("Localizable", "reset", fallback: "Reset")
|
||||||
/// Reset all settings back to defaults.
|
/// Reset all settings back to defaults.
|
||||||
internal static let resetAllSettings = L10n.tr("Localizable", "resetAllSettings", fallback: "Reset all settings back to defaults.")
|
internal static let resetAllSettings = L10n.tr("Localizable", "resetAllSettings", fallback: "Reset all settings back to defaults.")
|
||||||
|
/// Reset the filter values to none.
|
||||||
|
internal static let resetFilterFooter = L10n.tr("Localizable", "resetFilterFooter", fallback: "Reset the filter values to none.")
|
||||||
/// Reset Settings
|
/// Reset Settings
|
||||||
internal static let resetSettings = L10n.tr("Localizable", "resetSettings", fallback: "Reset Settings")
|
internal static let resetSettings = L10n.tr("Localizable", "resetSettings", fallback: "Reset Settings")
|
||||||
/// Reset Swiftfin user settings
|
/// Reset Swiftfin user settings
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
//
|
||||||
|
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
final class ServerActivityDetailViewModel: ViewModel, Stateful {
|
||||||
|
|
||||||
|
// MARK: - Action
|
||||||
|
|
||||||
|
enum Action: Equatable {
|
||||||
|
case refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - State
|
||||||
|
|
||||||
|
enum State: Hashable {
|
||||||
|
case error(JellyfinAPIError)
|
||||||
|
case initial
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Stateful Variables
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var backgroundStates: Set<BackgroundState> = []
|
||||||
|
@Published
|
||||||
|
var state: State = .initial
|
||||||
|
|
||||||
|
// MARK: - Published Variables
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var log: ActivityLogEntry
|
||||||
|
@Published
|
||||||
|
var user: UserDto?
|
||||||
|
@Published
|
||||||
|
var item: BaseItemDto?
|
||||||
|
|
||||||
|
// MARK: - Cancellable
|
||||||
|
|
||||||
|
private var getActivityCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
// MARK: - Initialize
|
||||||
|
|
||||||
|
init(log: ActivityLogEntry, user: UserDto?) {
|
||||||
|
self.log = log
|
||||||
|
self.user = user
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Respond
|
||||||
|
|
||||||
|
func respond(to action: Action) -> State {
|
||||||
|
switch action {
|
||||||
|
case .refresh:
|
||||||
|
getActivityCancellable?.cancel()
|
||||||
|
getActivityCancellable = Task {
|
||||||
|
do {
|
||||||
|
|
||||||
|
if let itemID = log.itemID {
|
||||||
|
self.item = try await getItem(for: itemID)
|
||||||
|
} else {
|
||||||
|
self.item = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let userID = log.userID {
|
||||||
|
self.user = try await getUser(for: userID)
|
||||||
|
} else {
|
||||||
|
self.user = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .error(.init(error.localizedDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.asAnyCancellable()
|
||||||
|
|
||||||
|
return .initial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Get the Activity's Item
|
||||||
|
|
||||||
|
private func getItem(for itemID: String) async throws -> BaseItemDto? {
|
||||||
|
let request = Paths.getItem(itemID: itemID)
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
return response.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Get the Activity's User
|
||||||
|
|
||||||
|
private func getUser(for userID: String) async throws -> UserDto? {
|
||||||
|
let request = Paths.getUserByID(userID: userID)
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
return response.value
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
//
|
||||||
|
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import IdentifiedCollections
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class ServerActivityViewModel: PagingLibraryViewModel<ActivityLogEntry> {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var hasUserId: Bool? {
|
||||||
|
didSet {
|
||||||
|
self.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var minDate: Date? {
|
||||||
|
didSet {
|
||||||
|
self.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var users: IdentifiedArrayOf<UserDto> = []
|
||||||
|
|
||||||
|
private var userTask: AnyCancellable?
|
||||||
|
|
||||||
|
override func respond(to action: Action) -> State {
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .refresh:
|
||||||
|
userTask?.cancel()
|
||||||
|
userTask = Task {
|
||||||
|
do {
|
||||||
|
let users = try await getUsers()
|
||||||
|
|
||||||
|
print("here")
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.users = users
|
||||||
|
_ = super.respond(to: action)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
self.send(.error(.init(L10n.unknownError)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.asAnyCancellable()
|
||||||
|
|
||||||
|
return .refreshing
|
||||||
|
default:
|
||||||
|
return super.respond(to: action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func get(page: Int) async throws -> [ActivityLogEntry] {
|
||||||
|
var parameters = Paths.GetLogEntriesParameters()
|
||||||
|
parameters.limit = pageSize
|
||||||
|
parameters.hasUserID = hasUserId
|
||||||
|
parameters.minDate = minDate
|
||||||
|
parameters.startIndex = page * pageSize
|
||||||
|
|
||||||
|
let request = Paths.getLogEntries(parameters: parameters)
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
return response.value.items ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getUsers() async throws -> IdentifiedArrayOf<UserDto> {
|
||||||
|
let request = Paths.getUsers()
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
return IdentifiedArray(uniqueElements: response.value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import IdentifiedCollections
|
import IdentifiedCollections
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import OrderedCollections
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable {
|
final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable {
|
||||||
|
|
|
@ -59,6 +59,16 @@
|
||||||
4E2AC4D42C6C4C1200DD600D /* OrderedSectionSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */; };
|
4E2AC4D42C6C4C1200DD600D /* OrderedSectionSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */; };
|
||||||
4E2AC4D62C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */; };
|
4E2AC4D62C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */; };
|
||||||
4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */; };
|
4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */; };
|
||||||
|
4E2CE3882DA424CE0004736A /* ServerActivityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE3872DA424CA0004736A /* ServerActivityViewModel.swift */; };
|
||||||
|
4E2CE38A2DA426720004736A /* ActivityLogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE3892DA426710004736A /* ActivityLogEntry.swift */; };
|
||||||
|
4E2CE38B2DA426720004736A /* ActivityLogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE3892DA426710004736A /* ActivityLogEntry.swift */; };
|
||||||
|
4E2CE38E2DA427880004736A /* ServerActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE38D2DA427870004736A /* ServerActivityView.swift */; };
|
||||||
|
4E2CE3912DA42B320004736A /* ServerActivityEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE3902DA42B280004736A /* ServerActivityEntry.swift */; };
|
||||||
|
4E2CE3932DA432C00004736A /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE3922DA432BC0004736A /* LogLevel.swift */; };
|
||||||
|
4E2CE3942DA432C00004736A /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE3922DA432BC0004736A /* LogLevel.swift */; };
|
||||||
|
4E2CE3982DA446900004736A /* ServerActivityDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE3972DA4468A0004736A /* ServerActivityDetailsView.swift */; };
|
||||||
|
4E2CE39B2DA479BC0004736A /* ServerActivityDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE39A2DA479AF0004736A /* ServerActivityDetailViewModel.swift */; };
|
||||||
|
4E2CE39F2DA4962A0004736A /* MediaItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CE39E2DA496270004736A /* MediaItemSection.swift */; };
|
||||||
4E31EFA12CFFFB1D0053DFE7 /* EditItemElementRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E31EFA02CFFFB180053DFE7 /* EditItemElementRow.swift */; };
|
4E31EFA12CFFFB1D0053DFE7 /* EditItemElementRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E31EFA02CFFFB180053DFE7 /* EditItemElementRow.swift */; };
|
||||||
4E31EFA52CFFFB690053DFE7 /* EditItemElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E31EFA42CFFFB670053DFE7 /* EditItemElementView.swift */; };
|
4E31EFA52CFFFB690053DFE7 /* EditItemElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E31EFA42CFFFB670053DFE7 /* EditItemElementView.swift */; };
|
||||||
4E35CE5C2CBED3F300DBD886 /* TimeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE562CBED3F300DBD886 /* TimeRow.swift */; };
|
4E35CE5C2CBED3F300DBD886 /* TimeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE562CBED3F300DBD886 /* TimeRow.swift */; };
|
||||||
|
@ -1318,6 +1328,14 @@
|
||||||
4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedSectionSelectorView.swift; sourceTree = "<group>"; };
|
4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedSectionSelectorView.swift; sourceTree = "<group>"; };
|
||||||
4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackQualitySettingsView.swift; sourceTree = "<group>"; };
|
4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackQualitySettingsView.swift; sourceTree = "<group>"; };
|
||||||
4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackQualitySettingsView.swift; sourceTree = "<group>"; };
|
4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackQualitySettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
4E2CE3872DA424CA0004736A /* ServerActivityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerActivityViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
4E2CE3892DA426710004736A /* ActivityLogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityLogEntry.swift; sourceTree = "<group>"; };
|
||||||
|
4E2CE38D2DA427870004736A /* ServerActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerActivityView.swift; sourceTree = "<group>"; };
|
||||||
|
4E2CE3902DA42B280004736A /* ServerActivityEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerActivityEntry.swift; sourceTree = "<group>"; };
|
||||||
|
4E2CE3922DA432BC0004736A /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = "<group>"; };
|
||||||
|
4E2CE3972DA4468A0004736A /* ServerActivityDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerActivityDetailsView.swift; sourceTree = "<group>"; };
|
||||||
|
4E2CE39A2DA479AF0004736A /* ServerActivityDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerActivityDetailViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
4E2CE39E2DA496270004736A /* MediaItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItemSection.swift; sourceTree = "<group>"; };
|
||||||
4E31EFA02CFFFB180053DFE7 /* EditItemElementRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditItemElementRow.swift; sourceTree = "<group>"; };
|
4E31EFA02CFFFB180053DFE7 /* EditItemElementRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditItemElementRow.swift; sourceTree = "<group>"; };
|
||||||
4E31EFA42CFFFB670053DFE7 /* EditItemElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditItemElementView.swift; sourceTree = "<group>"; };
|
4E31EFA42CFFFB670053DFE7 /* EditItemElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditItemElementView.swift; sourceTree = "<group>"; };
|
||||||
4E35CE532CBED3F300DBD886 /* DayOfWeekRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeekRow.swift; sourceTree = "<group>"; };
|
4E35CE532CBED3F300DBD886 /* DayOfWeekRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeekRow.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2308,6 +2326,8 @@
|
||||||
4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */,
|
4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */,
|
||||||
E101ECD42CD40489001EA89E /* DeviceDetailViewModel.swift */,
|
E101ECD42CD40489001EA89E /* DeviceDetailViewModel.swift */,
|
||||||
4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */,
|
4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */,
|
||||||
|
4E2CE39A2DA479AF0004736A /* ServerActivityDetailViewModel.swift */,
|
||||||
|
4E2CE3872DA424CA0004736A /* ServerActivityViewModel.swift */,
|
||||||
E1ED7FD72CA8AF7400ACB6E3 /* ServerTaskObserver.swift */,
|
E1ED7FD72CA8AF7400ACB6E3 /* ServerTaskObserver.swift */,
|
||||||
4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */,
|
4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */,
|
||||||
4EC2B1A42CC96F9F00D866BE /* ServerUserAdminViewModel.swift */,
|
4EC2B1A42CC96F9F00D866BE /* ServerUserAdminViewModel.swift */,
|
||||||
|
@ -2370,6 +2390,40 @@
|
||||||
path = MediaComponents;
|
path = MediaComponents;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4E2CE38C2DA427790004736A /* ServerActivity */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E2CE3962DA446810004736A /* ServerActivityDetailsView */,
|
||||||
|
4E2CE3952DA446730004736A /* ServerActivityView */,
|
||||||
|
);
|
||||||
|
path = ServerActivity;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4E2CE38F2DA42B250004736A /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E2CE3902DA42B280004736A /* ServerActivityEntry.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4E2CE3952DA446730004736A /* ServerActivityView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E2CE38F2DA42B250004736A /* Components */,
|
||||||
|
4E2CE38D2DA427870004736A /* ServerActivityView.swift */,
|
||||||
|
);
|
||||||
|
path = ServerActivityView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4E2CE3962DA446810004736A /* ServerActivityDetailsView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E2CE3972DA4468A0004736A /* ServerActivityDetailsView.swift */,
|
||||||
|
);
|
||||||
|
path = ServerActivityDetailsView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4E31EF972CFFB9B70053DFE7 /* Components */ = {
|
4E31EF972CFFB9B70053DFE7 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2551,6 +2605,7 @@
|
||||||
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */,
|
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */,
|
||||||
4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */,
|
4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */,
|
||||||
E1DE64902CC6F06C00E423B6 /* Components */,
|
E1DE64902CC6F06C00E423B6 /* Components */,
|
||||||
|
4E2CE38C2DA427790004736A /* ServerActivity */,
|
||||||
4EFE80852D3EF8270029CCB6 /* ServerDevices */,
|
4EFE80852D3EF8270029CCB6 /* ServerDevices */,
|
||||||
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
||||||
4ECF5D8C2D0A780F00F066B1 /* ServerTasks */,
|
4ECF5D8C2D0A780F00F066B1 /* ServerTasks */,
|
||||||
|
@ -5073,6 +5128,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */,
|
4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */,
|
||||||
|
4E2CE3892DA426710004736A /* ActivityLogEntry.swift */,
|
||||||
E1D37F5B2B9CF02600343D2B /* BaseItemDto */,
|
E1D37F5B2B9CF02600343D2B /* BaseItemDto */,
|
||||||
E1343DAC2D4EE4C8003145A8 /* BaseItemKind.swift */,
|
E1343DAC2D4EE4C8003145A8 /* BaseItemKind.swift */,
|
||||||
E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */,
|
E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */,
|
||||||
|
@ -5093,6 +5149,7 @@
|
||||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */,
|
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */,
|
||||||
E12A9EF729499E0100731C3A /* JellyfinClient.swift */,
|
E12A9EF729499E0100731C3A /* JellyfinClient.swift */,
|
||||||
4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */,
|
4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */,
|
||||||
|
4E2CE3922DA432BC0004736A /* LogLevel.swift */,
|
||||||
4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */,
|
4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */,
|
||||||
E1F5F9B12BA0200500BA5014 /* MediaSourceInfo */,
|
E1F5F9B12BA0200500BA5014 /* MediaSourceInfo */,
|
||||||
E122A9122788EAAD0060FA63 /* MediaStream.swift */,
|
E122A9122788EAAD0060FA63 /* MediaStream.swift */,
|
||||||
|
@ -5383,6 +5440,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E1DE64912CC6F0C900E423B6 /* DeviceSection.swift */,
|
E1DE64912CC6F0C900E423B6 /* DeviceSection.swift */,
|
||||||
|
4E2CE39E2DA496270004736A /* MediaItemSection.swift */,
|
||||||
4E10C81C2CC0465F0012CC9F /* UserSection.swift */,
|
4E10C81C2CC0465F0012CC9F /* UserSection.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
|
@ -6004,6 +6062,7 @@
|
||||||
E1A1529128FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */,
|
E1A1529128FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */,
|
||||||
E187A60529AD2E25008387E6 /* StepperView.swift in Sources */,
|
E187A60529AD2E25008387E6 /* StepperView.swift in Sources */,
|
||||||
E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */,
|
E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */,
|
||||||
|
4E2CE38B2DA426720004736A /* ActivityLogEntry.swift in Sources */,
|
||||||
4EF36F652D962A430065BB79 /* ItemSortBy.swift in Sources */,
|
4EF36F652D962A430065BB79 /* ItemSortBy.swift in Sources */,
|
||||||
E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */,
|
E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */,
|
||||||
E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
|
E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
|
||||||
|
@ -6019,6 +6078,7 @@
|
||||||
E1575E6A293E77B5001665B1 /* RoundedCorner.swift in Sources */,
|
E1575E6A293E77B5001665B1 /* RoundedCorner.swift in Sources */,
|
||||||
E102314B2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */,
|
E102314B2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */,
|
||||||
E1DD55382B6EE533007501C0 /* Task.swift in Sources */,
|
E1DD55382B6EE533007501C0 /* Task.swift in Sources */,
|
||||||
|
4E2CE3942DA432C00004736A /* LogLevel.swift in Sources */,
|
||||||
E1575EA1293E7B1E001665B1 /* String.swift in Sources */,
|
E1575EA1293E7B1E001665B1 /* String.swift in Sources */,
|
||||||
4E699BC02CB3477D007CBD5D /* HomeSection.swift in Sources */,
|
4E699BC02CB3477D007CBD5D /* HomeSection.swift in Sources */,
|
||||||
E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */,
|
E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */,
|
||||||
|
@ -6503,6 +6563,7 @@
|
||||||
4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */,
|
4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */,
|
||||||
4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */,
|
4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */,
|
||||||
4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */,
|
4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */,
|
||||||
|
4E2CE39B2DA479BC0004736A /* ServerActivityDetailViewModel.swift in Sources */,
|
||||||
4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */,
|
4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */,
|
||||||
4E661A162CEFE46300025C99 /* ParentialRatingsSection.swift in Sources */,
|
4E661A162CEFE46300025C99 /* ParentialRatingsSection.swift in Sources */,
|
||||||
4E661A172CEFE46300025C99 /* OverviewSection.swift in Sources */,
|
4E661A172CEFE46300025C99 /* OverviewSection.swift in Sources */,
|
||||||
|
@ -6527,6 +6588,7 @@
|
||||||
4EC2B1A52CC96FA400D866BE /* ServerUserAdminViewModel.swift in Sources */,
|
4EC2B1A52CC96FA400D866BE /* ServerUserAdminViewModel.swift in Sources */,
|
||||||
E1092F4C29106F9F00163F57 /* GestureAction.swift in Sources */,
|
E1092F4C29106F9F00163F57 /* GestureAction.swift in Sources */,
|
||||||
E11BDF772B8513B40045C54A /* ItemGenre.swift in Sources */,
|
E11BDF772B8513B40045C54A /* ItemGenre.swift in Sources */,
|
||||||
|
4E2CE3982DA446900004736A /* ServerActivityDetailsView.swift in Sources */,
|
||||||
E16DEAC228EFCF590058F196 /* EnvironmentValue+Keys.swift in Sources */,
|
E16DEAC228EFCF590058F196 /* EnvironmentValue+Keys.swift in Sources */,
|
||||||
E1BDF2F129524AB700CC0294 /* AutoPlayActionButton.swift in Sources */,
|
E1BDF2F129524AB700CC0294 /* AutoPlayActionButton.swift in Sources */,
|
||||||
E145EB452BE0AD4E003BF6F3 /* Set.swift in Sources */,
|
E145EB452BE0AD4E003BF6F3 /* Set.swift in Sources */,
|
||||||
|
@ -6673,9 +6735,11 @@
|
||||||
E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
|
E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
|
||||||
4EEEEA242CFA8E1500527D79 /* NavigationBarMenuButton.swift in Sources */,
|
4EEEEA242CFA8E1500527D79 /* NavigationBarMenuButton.swift in Sources */,
|
||||||
4EC2B1A92CC97C0700D866BE /* ServerUserDetailsView.swift in Sources */,
|
4EC2B1A92CC97C0700D866BE /* ServerUserDetailsView.swift in Sources */,
|
||||||
|
4E2CE3932DA432C00004736A /* LogLevel.swift in Sources */,
|
||||||
4EE07CBB2D08B19700B0B636 /* ErrorMessage.swift in Sources */,
|
4EE07CBB2D08B19700B0B636 /* ErrorMessage.swift in Sources */,
|
||||||
E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */,
|
E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */,
|
||||||
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */,
|
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */,
|
||||||
|
4E2CE39F2DA4962A0004736A /* MediaItemSection.swift in Sources */,
|
||||||
4E31EFA12CFFFB1D0053DFE7 /* EditItemElementRow.swift in Sources */,
|
4E31EFA12CFFFB1D0053DFE7 /* EditItemElementRow.swift in Sources */,
|
||||||
E1BE1CEA2BDB5AFE008176A9 /* UserGridButton.swift in Sources */,
|
E1BE1CEA2BDB5AFE008176A9 /* UserGridButton.swift in Sources */,
|
||||||
E1401CB129386C9200E8B599 /* UIColor.swift in Sources */,
|
E1401CB129386C9200E8B599 /* UIColor.swift in Sources */,
|
||||||
|
@ -6716,6 +6780,7 @@
|
||||||
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
||||||
E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
|
E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
|
||||||
4ECF5D8A2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */,
|
4ECF5D8A2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */,
|
||||||
|
4E2CE38E2DA427880004736A /* ServerActivityView.swift in Sources */,
|
||||||
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */,
|
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */,
|
||||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||||
E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */,
|
E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */,
|
||||||
|
@ -6783,6 +6848,7 @@
|
||||||
4E35CE5D2CBED3F300DBD886 /* TriggerTypeRow.swift in Sources */,
|
4E35CE5D2CBED3F300DBD886 /* TriggerTypeRow.swift in Sources */,
|
||||||
4E49DED32CE54D6D00352DCD /* ActiveSessionsPolicy.swift in Sources */,
|
4E49DED32CE54D6D00352DCD /* ActiveSessionsPolicy.swift in Sources */,
|
||||||
4E35CE5E2CBED3F300DBD886 /* AddTaskTriggerView.swift in Sources */,
|
4E35CE5E2CBED3F300DBD886 /* AddTaskTriggerView.swift in Sources */,
|
||||||
|
4E2CE3882DA424CE0004736A /* ServerActivityViewModel.swift in Sources */,
|
||||||
4E35CE5F2CBED3F300DBD886 /* IntervalRow.swift in Sources */,
|
4E35CE5F2CBED3F300DBD886 /* IntervalRow.swift in Sources */,
|
||||||
4E35CE602CBED3F300DBD886 /* DayOfWeekRow.swift in Sources */,
|
4E35CE602CBED3F300DBD886 /* DayOfWeekRow.swift in Sources */,
|
||||||
4E35CE612CBED3F300DBD886 /* TimeLimitSection.swift in Sources */,
|
4E35CE612CBED3F300DBD886 /* TimeLimitSection.swift in Sources */,
|
||||||
|
@ -6922,6 +6988,7 @@
|
||||||
E1AD104D26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
E1AD104D26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
||||||
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
||||||
4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */,
|
4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */,
|
||||||
|
4E2CE38A2DA426720004736A /* ActivityLogEntry.swift in Sources */,
|
||||||
535870AD2669D8DD00D05A09 /* ItemFilterCollection.swift in Sources */,
|
535870AD2669D8DD00D05A09 /* ItemFilterCollection.swift in Sources */,
|
||||||
E1D27EE72BBC955F00152D16 /* UnmaskSecureField.swift in Sources */,
|
E1D27EE72BBC955F00152D16 /* UnmaskSecureField.swift in Sources */,
|
||||||
E1CAF65D2BA345830087D991 /* MediaType.swift in Sources */,
|
E1CAF65D2BA345830087D991 /* MediaType.swift in Sources */,
|
||||||
|
@ -6963,6 +7030,7 @@
|
||||||
E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
||||||
E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
||||||
4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */,
|
4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */,
|
||||||
|
4E2CE3912DA42B320004736A /* ServerActivityEntry.swift in Sources */,
|
||||||
E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */,
|
E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */,
|
||||||
E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */,
|
E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */,
|
||||||
E133328F2953B71000EE76AB /* DownloadTaskView.swift in Sources */,
|
E133328F2953B71000EE76AB /* DownloadTaskView.swift in Sources */,
|
||||||
|
|
|
@ -53,7 +53,7 @@ struct ActiveSessionDetailView: View {
|
||||||
) -> some View {
|
) -> some View {
|
||||||
List {
|
List {
|
||||||
|
|
||||||
nowPlayingSection(item: nowPlayingItem)
|
AdminDashboardView.MediaItemSection(item: nowPlayingItem)
|
||||||
|
|
||||||
Section(L10n.progress) {
|
Section(L10n.progress) {
|
||||||
ActiveSessionsView.ProgressSection(
|
ActiveSessionsView.ProgressSection(
|
||||||
|
@ -101,67 +101,6 @@ struct ActiveSessionDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Now Playing Section
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func nowPlayingSection(item: BaseItemDto) -> some View {
|
|
||||||
Section {
|
|
||||||
HStack(alignment: .bottom, spacing: 12) {
|
|
||||||
Group {
|
|
||||||
if item.type == .audio {
|
|
||||||
ZStack {
|
|
||||||
Color.clear
|
|
||||||
|
|
||||||
ImageView(item.squareImageSources(maxWidth: 60))
|
|
||||||
.failure {
|
|
||||||
SystemImageContentView(systemName: item.systemImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.squarePosterStyle()
|
|
||||||
} else {
|
|
||||||
ZStack {
|
|
||||||
Color.clear
|
|
||||||
|
|
||||||
ImageView(item.portraitImageSources(maxWidth: 60))
|
|
||||||
.failure {
|
|
||||||
SystemImageContentView(systemName: item.systemImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.posterStyle(.portrait)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: 100)
|
|
||||||
.accessibilityIgnoresInvertColors()
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
|
|
||||||
if let parent = item.parentTitle {
|
|
||||||
Text(parent)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(item.displayTitle)
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
.lineLimit(2)
|
|
||||||
|
|
||||||
if let subtitle = item.subtitle {
|
|
||||||
Text(subtitle)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listRowBackground(Color.clear)
|
|
||||||
.listRowCornerRadius(0)
|
|
||||||
.listRowInsets(.zero)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if let session = box.value {
|
if let session = box.value {
|
||||||
|
|
|
@ -28,6 +28,9 @@ struct AdminDashboardView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(L10n.activity) {
|
Section(L10n.activity) {
|
||||||
|
ChevronButton(L10n.activity) {
|
||||||
|
router.route(to: \.activity)
|
||||||
|
}
|
||||||
ChevronButton(L10n.devices) {
|
ChevronButton(L10n.devices) {
|
||||||
router.route(to: \.devices)
|
router.route(to: \.devices)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
//
|
||||||
|
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension AdminDashboardView {
|
||||||
|
|
||||||
|
struct MediaItemSection: View {
|
||||||
|
|
||||||
|
let item: BaseItemDto
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
HStack(alignment: .bottom, spacing: 12) {
|
||||||
|
Group {
|
||||||
|
if item.type == .audio {
|
||||||
|
ZStack {
|
||||||
|
Color.clear
|
||||||
|
|
||||||
|
ImageView(item.squareImageSources(maxWidth: 60))
|
||||||
|
.failure {
|
||||||
|
SystemImageContentView(systemName: item.systemImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.squarePosterStyle()
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
Color.clear
|
||||||
|
|
||||||
|
ImageView(item.portraitImageSources(maxWidth: 60))
|
||||||
|
.failure {
|
||||||
|
SystemImageContentView(systemName: item.systemImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.posterStyle(.portrait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 100)
|
||||||
|
.accessibilityIgnoresInvertColors()
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
if let parent = item.parentTitle {
|
||||||
|
Text(parent)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(item.displayTitle)
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
if let subtitle = item.subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.listRowCornerRadius(0)
|
||||||
|
.listRowInsets(.zero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
//
|
||||||
|
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ServerActivityDetailsView: View {
|
||||||
|
|
||||||
|
// MARK: - Environment Objects
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: AdminDashboardCoordinator.Router
|
||||||
|
|
||||||
|
// MARK: - Activity Log Entry Variable
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
var viewModel: ServerActivityDetailViewModel
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
/// Item (If Available)
|
||||||
|
if let item = viewModel.item {
|
||||||
|
AdminDashboardView.MediaItemSection(item: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User (If Available)
|
||||||
|
if let user = viewModel.user {
|
||||||
|
AdminDashboardView.UserSection(
|
||||||
|
user: user,
|
||||||
|
lastActivityDate: viewModel.log.date
|
||||||
|
) {
|
||||||
|
router.route(to: \.userDetails, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event Name & Overview
|
||||||
|
Section(L10n.overview) {
|
||||||
|
if let name = viewModel.log.name, name.isNotEmpty {
|
||||||
|
Text(name)
|
||||||
|
}
|
||||||
|
if let overview = viewModel.log.overview, overview.isNotEmpty {
|
||||||
|
Text(overview)
|
||||||
|
} else if let shortOverview = viewModel.log.shortOverview, shortOverview.isNotEmpty {
|
||||||
|
Text(shortOverview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event Details
|
||||||
|
Section(L10n.details) {
|
||||||
|
if let severity = viewModel.log.severity {
|
||||||
|
TextPairView(
|
||||||
|
leading: L10n.level,
|
||||||
|
trailing: severity.displayTitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if let type = viewModel.log.type {
|
||||||
|
TextPairView(
|
||||||
|
leading: L10n.type,
|
||||||
|
trailing: type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if let date = viewModel.log.date {
|
||||||
|
TextPairView(
|
||||||
|
leading: L10n.date,
|
||||||
|
trailing: date.formatted(date: .long, time: .shortened)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.navigationTitle(
|
||||||
|
L10n.activityLog
|
||||||
|
.localizedCapitalized
|
||||||
|
)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.onFirstAppear {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
//
|
||||||
|
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension ServerActivityView {
|
||||||
|
|
||||||
|
struct LogEntry: View {
|
||||||
|
|
||||||
|
// MARK: - Activity Log Entry Variable
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
var viewModel: ServerActivityDetailViewModel
|
||||||
|
|
||||||
|
// MARK: - Action Variable
|
||||||
|
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ListRow {
|
||||||
|
userImage
|
||||||
|
} content: {
|
||||||
|
rowContent
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
}
|
||||||
|
.onSelect(perform: action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - User Image
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var userImage: some View {
|
||||||
|
let imageSource = viewModel.user?.profileImageSource(client: viewModel.userSession.client, maxWidth: 60) ?? .init()
|
||||||
|
|
||||||
|
UserProfileImage(
|
||||||
|
userID: viewModel.log.userID ?? viewModel.userSession?.user.id,
|
||||||
|
source: imageSource
|
||||||
|
) {
|
||||||
|
SystemImageContentView(
|
||||||
|
systemName: viewModel.user != nil ? "person.fill" : "gearshape.fill",
|
||||||
|
ratio: 0.5
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - User Image
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var rowContent: some View {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
/// Event Severity & Username / System
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: viewModel.log.severity?.systemImage ?? "questionmark.circle")
|
||||||
|
.foregroundStyle(viewModel.log.severity?.color ?? .gray)
|
||||||
|
|
||||||
|
if viewModel.user != nil {
|
||||||
|
Text(viewModel.user?.name ?? L10n.unknown)
|
||||||
|
} else {
|
||||||
|
Text(L10n.system)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
/// Event Name
|
||||||
|
Text(viewModel.log.name ?? .emptyDash)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(2)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
|
Group {
|
||||||
|
if let eventDate = viewModel.log.date {
|
||||||
|
Text(eventDate.formatted(date: .abbreviated, time: .standard))
|
||||||
|
} else {
|
||||||
|
Text(String.emptyTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.padding()
|
||||||
|
.font(.body.weight(.regular))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,198 @@
|
||||||
|
//
|
||||||
|
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import CollectionVGrid
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ServerActivityView: View {
|
||||||
|
|
||||||
|
// MARK: - Environment Objects
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: AdminDashboardCoordinator.Router
|
||||||
|
|
||||||
|
// MARK: - State Objects
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var viewModel = ServerActivityViewModel()
|
||||||
|
|
||||||
|
// MARK: - Dialog States
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isDatePickerShowing: Bool = false
|
||||||
|
@State
|
||||||
|
private var tempDate: Date?
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
switch viewModel.state {
|
||||||
|
case .content:
|
||||||
|
contentView
|
||||||
|
case let .error(error):
|
||||||
|
ErrorView(error: error)
|
||||||
|
.onRetry {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
case .initial, .refreshing:
|
||||||
|
DelayedProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.linear(duration: 0.2), value: viewModel.state)
|
||||||
|
.navigationTitle(L10n.activity)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarMenuButton(
|
||||||
|
isLoading: viewModel.backgroundStates.contains(.gettingNextPage)
|
||||||
|
) {
|
||||||
|
Section(L10n.filters) {
|
||||||
|
startDateButton
|
||||||
|
userFilterButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFirstAppear {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $isDatePickerShowing, onDismiss: { isDatePickerShowing = false }) {
|
||||||
|
startDatePickerSheet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Content View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var contentView: some View {
|
||||||
|
if viewModel.elements.isEmpty {
|
||||||
|
Text(L10n.none)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowInsets(.zero)
|
||||||
|
} else {
|
||||||
|
CollectionVGrid(
|
||||||
|
uniqueElements: viewModel.elements,
|
||||||
|
id: \.unwrappedIDHashOrZero,
|
||||||
|
layout: .columns(1)
|
||||||
|
) { log in
|
||||||
|
|
||||||
|
let user = viewModel.users.first(
|
||||||
|
property: \.id,
|
||||||
|
equalTo: log.userID
|
||||||
|
)
|
||||||
|
|
||||||
|
let logViewModel = ServerActivityDetailViewModel(
|
||||||
|
log: log,
|
||||||
|
user: user
|
||||||
|
)
|
||||||
|
|
||||||
|
LogEntry(viewModel: logViewModel) {
|
||||||
|
router.route(to: \.activityDetails, logViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReachedBottomEdge(offset: .offset(300)) {
|
||||||
|
viewModel.send(.getNextPage)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - User Filter Button
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var userFilterButton: some View {
|
||||||
|
Menu(
|
||||||
|
L10n.type,
|
||||||
|
systemImage: viewModel.hasUserId == true ? "person.fill" :
|
||||||
|
viewModel.hasUserId == false ? "gearshape.fill" : "line.3.horizontal"
|
||||||
|
) {
|
||||||
|
Picker(L10n.type, selection: $viewModel.hasUserId) {
|
||||||
|
Section {
|
||||||
|
Label(
|
||||||
|
L10n.all,
|
||||||
|
systemImage: "line.3.horizontal"
|
||||||
|
)
|
||||||
|
.tag(nil as Bool?)
|
||||||
|
}
|
||||||
|
|
||||||
|
Label(
|
||||||
|
L10n.users,
|
||||||
|
systemImage: "person"
|
||||||
|
)
|
||||||
|
.tag(true as Bool?)
|
||||||
|
|
||||||
|
Label(
|
||||||
|
L10n.system,
|
||||||
|
systemImage: "gearshape"
|
||||||
|
)
|
||||||
|
.tag(false as Bool?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Start Date Button
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var startDateButton: some View {
|
||||||
|
Button(L10n.startDate, systemImage: "calendar") {
|
||||||
|
if let minDate = viewModel.minDate {
|
||||||
|
tempDate = minDate
|
||||||
|
} else {
|
||||||
|
tempDate = .now
|
||||||
|
}
|
||||||
|
isDatePickerShowing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Start Date Picker Sheet
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var startDatePickerSheet: some View {
|
||||||
|
NavigationView {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
DatePicker(
|
||||||
|
L10n.date,
|
||||||
|
selection: $tempDate.coalesce(.now),
|
||||||
|
in: ...Date.now,
|
||||||
|
displayedComponents: .date
|
||||||
|
)
|
||||||
|
.datePickerStyle(.graphical)
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset button to remove the filter
|
||||||
|
if viewModel.minDate != nil {
|
||||||
|
Section {
|
||||||
|
ListRowButton(L10n.reset, role: .destructive) {
|
||||||
|
viewModel.minDate = nil
|
||||||
|
isDatePickerShowing = false
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
Text(L10n.resetFilterFooter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(L10n.startDate.localizedCapitalized)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarCloseButton {
|
||||||
|
isDatePickerShowing = false
|
||||||
|
}
|
||||||
|
.topBarTrailing {
|
||||||
|
let startOfDay = Calendar.current
|
||||||
|
.startOfDay(for: tempDate ?? .now)
|
||||||
|
|
||||||
|
Button(L10n.save) {
|
||||||
|
viewModel.minDate = startOfDay
|
||||||
|
isDatePickerShowing = false
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.disabled(viewModel.minDate != nil && startOfDay == viewModel.minDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Loading…
Reference in New Issue