[iOS] Admin Dashboard - Add/Delete Task Triggers (#1276)
Some checks are pending
Build 🔨 / Build 🔨 (Swiftfin tvOS) (push) Waiting to run
Build 🔨 / Build 🔨 (Swiftfin) (push) Waiting to run

* All Working. TODO: Figure out why TimeInterval crashes Swiftfin if I select 'Cancel'

* Cleanup. Kind of a typeAlias but not really? Fixed the minute crash, I was make a recursive calc. All good now. Make sure temp values default to existing value at startup

* Manual Run action from Edit View

* Issues resolved.

* Labels / soft merge with Main

* Utilize events to print a success/failure message for when there is an attempted change with a TaskTrigger.

* Fix label wrong value & remove TODO for completed item.

* Fix all the merge issues.

* wip

* wip

* localize

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe 2024-10-23 16:30:12 -06:00 committed by GitHub
parent a04f97e1ba
commit c46ee13dbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1612 additions and 205 deletions

View File

@ -49,15 +49,21 @@ final class SettingsCoordinator: NavigationCoordinatable {
var videoPlayerSettings = makeVideoPlayerSettings
@Route(.push)
var customDeviceProfileSettings = makeCustomDeviceProfileSettings
@Route(.modal)
var itemOverviewView = makeItemOverviewView
@Route(.modal)
var editCustomDeviceProfile = makeEditCustomDeviceProfile
@Route(.modal)
var createCustomDeviceProfile = makeCreateCustomDeviceProfile
// TODO: Move AdminDashboard items to its own coordinator ->
@Route(.push)
var userDashboard = makeUserDashboard
@Route(.push)
var activeSessions = makeActiveSessions
@Route(.push)
var activeDeviceDetails = makeActiveDeviceDetails
@Route(.modal)
var itemOverviewView = makeItemOverviewView
@Route(.push)
var tasks = makeTasks
@Route(.push)
@ -65,14 +71,12 @@ final class SettingsCoordinator: NavigationCoordinatable {
@Route(.push)
var deviceDetails = makeDeviceDetails
@Route(.push)
var editScheduledTask = makeEditScheduledTask
var editServerTask = makeEditServerTask
@Route(.modal)
var addServerTaskTrigger = makeAddServerTaskTrigger
@Route(.push)
var serverLogs = makeServerLogs
@Route(.modal)
var editCustomDeviceProfile = makeEditCustomDeviceProfile
@Route(.modal)
var createCustomDeviceProfile = makeCreateCustomDeviceProfile
// <- End of AdminDashboard Items
#if DEBUG
@Route(.push)
@ -164,6 +168,22 @@ final class SettingsCoordinator: NavigationCoordinatable {
EditServerView(server: server)
}
func makeItemOverviewView(item: BaseItemDto) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
ItemOverviewView(item: item)
}
}
func makeItemFilterDrawerSelector(selection: Binding<[ItemFilterType]>) -> some View {
OrderedSectionSelectorView(selection: selection, sources: ItemFilterType.allCases)
.navigationTitle(L10n.filters)
}
func makeVideoPlayerSettings() -> VideoPlayerSettingsCoordinator {
VideoPlayerSettingsCoordinator()
}
// TODO: Move AdminDashboard items to its own coordinator ->
@ViewBuilder
func makeUserDashboard() -> some View {
UserDashboardView()
@ -179,15 +199,9 @@ final class SettingsCoordinator: NavigationCoordinatable {
ActiveSessionDetailView(box: box)
}
func makeItemOverviewView(item: BaseItemDto) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
ItemOverviewView(item: item)
}
}
@ViewBuilder
func makeTasks() -> some View {
ScheduledTasksView()
ServerTasksView()
}
@ViewBuilder
@ -201,8 +215,14 @@ final class SettingsCoordinator: NavigationCoordinatable {
}
@ViewBuilder
func makeEditScheduledTask(observer: ServerTaskObserver) -> some View {
EditScheduledTaskView(observer: observer)
func makeEditServerTask(observer: ServerTaskObserver) -> some View {
EditServerTaskView(observer: observer)
}
func makeAddServerTaskTrigger(observer: ServerTaskObserver) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
AddTaskTriggerView(observer: observer)
}
}
@ViewBuilder
@ -210,14 +230,7 @@ final class SettingsCoordinator: NavigationCoordinatable {
ServerLogsView()
}
func makeItemFilterDrawerSelector(selection: Binding<[ItemFilterType]>) -> some View {
OrderedSectionSelectorView(selection: selection, sources: ItemFilterType.allCases)
.navigationTitle(L10n.filters)
}
func makeVideoPlayerSettings() -> VideoPlayerSettingsCoordinator {
VideoPlayerSettingsCoordinator()
}
// <- End of AdminDashboard Items
#if DEBUG
@ViewBuilder

View File

@ -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 Foundation
import JellyfinAPI
extension DayOfWeek {
var displayTitle: String? {
let newLineRemoved = rawValue.replacingOccurrences(of: "\n", with: "")
guard let index = DateFormatter().weekdaySymbols.firstIndex(of: newLineRemoved) else {
return nil
}
return Calendar.current
.weekdaySymbols[index]
.localizedCapitalized
}
}

View File

@ -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) 2024 Jellyfin & Jellyfin Contributors
//
import Foundation
// TODO: remove and have sdk use strong types instead
typealias ServerTicks = Int
extension ServerTicks {
// MARK: - Conversion Constants
private static let ticksPerSecond = 10_000_000
private static let ticksPerMinute = 600_000_000
private static let ticksPerHour = 36_000_000_000
private static let ticksPerDay = 864_000_000_000
// MARK: - Initializers
init(_ ticks: Int? = nil) {
self = ticks ?? 0
}
init(seconds: Int? = nil) {
self = (seconds ?? 0) * ServerTicks.ticksPerSecond
}
init(minutes: Int? = nil) {
self = (minutes ?? 0) * ServerTicks.ticksPerMinute
}
init(hours: Int? = nil) {
self = (hours ?? 0) * ServerTicks.ticksPerHour
}
init(days: Int? = nil) {
self = (days ?? 0) * ServerTicks.ticksPerDay
}
init(timeInterval: TimeInterval? = nil) {
self = Int((timeInterval ?? 0) * Double(ServerTicks.ticksPerSecond))
}
init(date: Date) {
let components = Calendar.current.dateComponents([.hour, .minute], from: date)
let totalSeconds = TimeInterval((components.hour ?? 0) * 3600 + (components.minute ?? 0) * 60)
self = Int(totalSeconds * 10_000_000)
}
// MARK: - Computed Properties
var ticks: Int {
self
}
var seconds: TimeInterval {
TimeInterval(self) / Double(ServerTicks.ticksPerSecond)
}
var minutes: TimeInterval {
TimeInterval(self) / Double(ServerTicks.ticksPerMinute)
}
var hours: TimeInterval {
TimeInterval(self) / Double(ServerTicks.ticksPerHour)
}
var days: TimeInterval {
TimeInterval(self) / Double(ServerTicks.ticksPerDay)
}
var date: Date {
let totalSeconds = TimeInterval(self) / 10_000_000
let hours = Int(totalSeconds) / 3600
let minutes = (Int(totalSeconds) % 3600) / 60
var components = DateComponents()
components.hour = hours
components.minute = minutes
return Calendar.current.date(from: components) ?? Date()
}
}

View File

@ -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) 2024 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
extension TaskState: Displayable {
var displayTitle: String {
switch self {
case .cancelling:
return L10n.cancelling
case .idle:
return L10n.idle
case .running:
return L10n.running
}
}
}

View File

@ -0,0 +1,45 @@
//
// 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
// TODO: move to SDK as patch file
enum TaskTriggerType: String, Codable, CaseIterable, Displayable, SystemImageable {
case daily = "DailyTrigger"
case weekly = "WeeklyTrigger"
case interval = "IntervalTrigger"
case startup = "StartupTrigger"
var displayTitle: String {
switch self {
case .daily:
return L10n.daily
case .weekly:
return L10n.weekly
case .interval:
return L10n.interval
case .startup:
return L10n.onApplicationStartup
}
}
var systemImage: String {
switch self {
case .daily:
return "clock"
case .weekly:
return "calendar"
case .interval:
return "timer"
case .startup:
return "power"
}
}
}

View File

@ -20,8 +20,12 @@ internal enum L10n {
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
/// ActiveSessionsView Header
internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices")
/// Add
internal static let add = L10n.tr("Localizable", "add", fallback: "Add")
/// Select Server View - Add Server
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
/// Add trigger
internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add trigger")
/// Add URL
internal static let addURL = L10n.tr("Localizable", "addURL", fallback: "Add URL")
/// Administration Dashboard Section
@ -150,6 +154,8 @@ internal enum L10n {
internal static let category = L10n.tr("Localizable", "category", fallback: "Category")
/// Change Server
internal static let changeServer = L10n.tr("Localizable", "changeServer", fallback: "Change Server")
/// Changes not saved
internal static let changesNotSaved = L10n.tr("Localizable", "changesNotSaved", fallback: "Changes not saved")
/// Channels
internal static let channels = L10n.tr("Localizable", "channels", fallback: "Channels")
/// Chapters
@ -234,14 +240,18 @@ internal enum L10n {
internal static let customize = L10n.tr("Localizable", "customize", fallback: "Customize")
/// Section Header for a Custom Device Profile
internal static let customProfile = L10n.tr("Localizable", "customProfile", fallback: "Custom Profile")
/// Daily
internal static let daily = L10n.tr("Localizable", "daily", fallback: "Daily")
/// Represents the dark theme setting
internal static let dark = L10n.tr("Localizable", "dark", fallback: "Dark")
/// UserDashboardView Header
internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard")
/// Description for the dashboard section
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.")
/// Day of Week
internal static let dayOfWeek = L10n.tr("Localizable", "dayOfWeek", fallback: "Day of Week")
/// Time Interval Help Text - Days
internal static let days = L10n.tr("Localizable", "days", fallback: "days")
internal static let days = L10n.tr("Localizable", "days", fallback: "Days")
/// Default Scheme
internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "Default Scheme")
/// Server Detail View - Delete
@ -262,8 +272,14 @@ internal enum L10n {
internal static let deleteSelectionDevicesWarning = L10n.tr("Localizable", "deleteSelectionDevicesWarning", fallback: "Are you sure you wish to delete all selected devices? All selected sessions will be logged out.")
/// Server Detail View - Delete Server
internal static let deleteServer = L10n.tr("Localizable", "deleteServer", fallback: "Delete Server")
/// Delete Trigger
internal static let deleteTrigger = L10n.tr("Localizable", "deleteTrigger", fallback: "Delete Trigger")
/// Are you sure you want to delete this trigger? This action cannot be undone.
internal static let deleteTriggerConfirmationMessage = L10n.tr("Localizable", "deleteTriggerConfirmationMessage", fallback: "Are you sure you want to delete this trigger? This action cannot be undone.")
/// Delivery
internal static let delivery = L10n.tr("Localizable", "delivery", fallback: "Delivery")
/// Details
internal static let details = L10n.tr("Localizable", "details", fallback: "Details")
/// Session Device Section Label
internal static let device = L10n.tr("Localizable", "device", fallback: "Device")
/// Section Header for Device Profiles
@ -282,6 +298,8 @@ internal enum L10n {
internal static let directStream = L10n.tr("Localizable", "directStream", fallback: "Direct Stream")
/// Disabled
internal static let disabled = L10n.tr("Localizable", "disabled", fallback: "Disabled")
/// Discard Changes
internal static let discardChanges = L10n.tr("Localizable", "discardChanges", fallback: "Discard Changes")
/// Discovered Servers
internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers", fallback: "Discovered Servers")
/// Dismiss
@ -312,6 +330,16 @@ internal enum L10n {
internal static let episodes = L10n.tr("Localizable", "episodes", fallback: "Episodes")
/// Error
internal static let error = L10n.tr("Localizable", "error", fallback: "Error")
/// Error Details
internal static let errorDetails = L10n.tr("Localizable", "errorDetails", fallback: "Error Details")
/// Every
internal static let every = L10n.tr("Localizable", "every", fallback: "Every")
/// Every %1$@
internal static func everyInterval(_ p1: Any) -> String {
return L10n.tr("Localizable", "everyInterval", String(describing: p1), fallback: "Every %1$@")
}
/// Executed
internal static let executed = L10n.tr("Localizable", "executed", fallback: "Executed")
/// Existing Server
internal static let existingServer = L10n.tr("Localizable", "existingServer", fallback: "Existing Server")
/// Existing User
@ -344,16 +372,26 @@ internal enum L10n {
internal static let hapticFeedback = L10n.tr("Localizable", "hapticFeedback", fallback: "Haptic Feedback")
/// Home
internal static let home = L10n.tr("Localizable", "home", fallback: "Home")
/// Hours
internal static let hours = L10n.tr("Localizable", "hours", fallback: "Hours")
/// Idle
internal static let idle = L10n.tr("Localizable", "idle", fallback: "Idle")
/// Customize Server View - Indicators
internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators")
/// Information
internal static let information = L10n.tr("Localizable", "information", fallback: "Information")
/// TranscodeReason - Interlaced Video Not Supported
internal static let interlacedVideoNotSupported = L10n.tr("Localizable", "interlacedVideoNotSupported", fallback: "Interlaced video is not supported")
/// Interval
internal static let interval = L10n.tr("Localizable", "interval", fallback: "Interval")
/// Inverted Dark
internal static let invertedDark = L10n.tr("Localizable", "invertedDark", fallback: "Inverted Dark")
/// Inverted Light
internal static let invertedLight = L10n.tr("Localizable", "invertedLight", fallback: "Inverted Light")
/// %1$@ at %2$@
internal static func itemAtItem(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "itemAtItem", String(describing: p1), String(describing: p2), fallback: "%1$@ at %2$@")
}
/// SessionPlaybackMethod Remaining Time
internal static func itemOverItem(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "itemOverItem", String(describing: p1), String(describing: p2), fallback: "%1$@ / %2$@")
@ -420,6 +458,8 @@ internal enum L10n {
}
/// Settings View - Logs
internal static let logs = L10n.tr("Localizable", "logs", fallback: "Logs")
/// Access the Jellyfin server logs for troubleshooting and monitoring purposes.
internal static let logsDescription = L10n.tr("Localizable", "logsDescription", fallback: "Access the Jellyfin server logs for troubleshooting and monitoring purposes.")
/// Option to set the maximum bitrate for playback
internal static let maximumBitrate = L10n.tr("Localizable", "maximumBitrate", fallback: "Maximum Bitrate")
/// Playback May Fail
@ -430,6 +470,8 @@ internal enum L10n {
internal static let menuButtons = L10n.tr("Localizable", "menuButtons", fallback: "Menu Buttons")
/// The play method (e.g., Direct Play, Transcoding)
internal static let method = L10n.tr("Localizable", "method", fallback: "Method")
/// Minutes
internal static let minutes = L10n.tr("Localizable", "minutes", fallback: "Minutes")
/// Missing
internal static let missing = L10n.tr("Localizable", "missing", fallback: "Missing")
/// Missing Items
@ -488,6 +530,8 @@ internal enum L10n {
internal static let noResults = L10n.tr("Localizable", "noResults", fallback: "No results.")
/// Normal
internal static let normal = L10n.tr("Localizable", "normal", fallback: "Normal")
/// No runtime limit
internal static let noRuntimeLimit = L10n.tr("Localizable", "noRuntimeLimit", fallback: "No runtime limit")
/// No active session available
internal static let noSession = L10n.tr("Localizable", "noSession", fallback: "No session")
/// N/A
@ -502,6 +546,8 @@ internal enum L10n {
internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset")
/// Ok
internal static let ok = L10n.tr("Localizable", "ok", fallback: "Ok")
/// On application startup
internal static let onApplicationStartup = L10n.tr("Localizable", "onApplicationStartup", fallback: "On application startup")
/// 1 user
internal static let oneUser = L10n.tr("Localizable", "oneUser", fallback: "1 user")
/// Indicates that something is Online
@ -674,7 +720,7 @@ internal enum L10n {
internal static let running = L10n.tr("Localizable", "running", fallback: "Running...")
/// Runtime
internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime")
/// Save - Completed, end, or save
/// Save
internal static let save = L10n.tr("Localizable", "save", fallback: "Save")
/// Administration Dashboard Scan All Libraries Button
internal static let scanAllLibraries = L10n.tr("Localizable", "scanAllLibraries", fallback: "Scan All Libraries")
@ -730,6 +776,14 @@ internal enum L10n {
internal static let serverLogs = L10n.tr("Localizable", "serverLogs", fallback: "Server Logs")
/// Select Server View
internal static let servers = L10n.tr("Localizable", "servers", fallback: "Servers")
/// A new trigger was created for '%1$@'.
internal static func serverTriggerCreated(_ p1: Any) -> String {
return L10n.tr("Localizable", "serverTriggerCreated", String(describing: p1), fallback: "A new trigger was created for '%1$@'.")
}
/// The selected trigger was deleted from '%1$@'.
internal static func serverTriggerDeleted(_ p1: Any) -> String {
return L10n.tr("Localizable", "serverTriggerDeleted", String(describing: p1), fallback: "The selected trigger was deleted from '%1$@'.")
}
/// Server URL
internal static let serverURL = L10n.tr("Localizable", "serverURL", fallback: "Server URL")
/// The title for the session view
@ -796,6 +850,8 @@ internal enum L10n {
internal static let specialFeatures = L10n.tr("Localizable", "specialFeatures", fallback: "Special Features")
/// Sports
internal static let sports = L10n.tr("Localizable", "sports", fallback: "Sports")
/// Status
internal static let status = L10n.tr("Localizable", "status", fallback: "Status")
/// Button label to stop a task
internal static let stop = L10n.tr("Localizable", "stop", fallback: "Stop")
/// Session Streaming Clients
@ -854,8 +910,24 @@ internal enum L10n {
internal static let tasks = L10n.tr("Localizable", "tasks", fallback: "Tasks")
/// Description for the tasks section
internal static let tasksDescription = L10n.tr("Localizable", "tasksDescription", fallback: "Tasks are operations that are scheduled to run periodically or can be triggered manually.")
/// Sets the duration (in minutes) in between task triggers.
internal static let taskTriggerInterval = L10n.tr("Localizable", "taskTriggerInterval", fallback: "Sets the duration (in minutes) in between task triggers.")
/// Sets the maximum runtime (in hours) for this task trigger.
internal static let taskTriggerTimeLimit = L10n.tr("Localizable", "taskTriggerTimeLimit", fallback: "Sets the maximum runtime (in hours) for this task trigger.")
/// Option to set the test size for bitrate testing
internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "Test Size")
/// Time
internal static let time = L10n.tr("Localizable", "time", fallback: "Time")
/// Time Limit
internal static let timeLimit = L10n.tr("Localizable", "timeLimit", fallback: "Time Limit")
/// Time limit: %1$@
internal static func timeLimitLabelWithValue(_ p1: Any) -> String {
return L10n.tr("Localizable", "timeLimitLabelWithValue", String(describing: p1), fallback: "Time limit: %1$@")
}
/// Time Limit (%@)
internal static func timeLimitWithUnit(_ p1: Any) -> String {
return L10n.tr("Localizable", "timeLimitWithUnit", String(describing: p1), fallback: "Time Limit (%@)")
}
/// Timestamp
internal static let timestamp = L10n.tr("Localizable", "timestamp", fallback: "Timestamp")
/// Timestamp Type
@ -870,10 +942,16 @@ internal enum L10n {
internal static let transcodeReasons = L10n.tr("Localizable", "transcodeReasons", fallback: "Transcode Reason(s)")
/// Transition
internal static let transition = L10n.tr("Localizable", "transition", fallback: "Transition")
/// Trigger already exists
internal static let triggerAlreadyExists = L10n.tr("Localizable", "triggerAlreadyExists", fallback: "Trigger already exists")
/// Triggers
internal static let triggers = L10n.tr("Localizable", "triggers", fallback: "Triggers")
/// Try again
internal static let tryAgain = L10n.tr("Localizable", "tryAgain", fallback: "Try again")
/// TV Shows
internal static let tvShows = L10n.tr("Localizable", "tvShows", fallback: "TV Shows")
/// Indicate a type
internal static let type = L10n.tr("Localizable", "type", fallback: "Type")
/// Unable to connect to server
internal static let unableToConnectServer = L10n.tr("Localizable", "unableToConnectServer", fallback: "Unable to connect to server")
/// Unable to find host
@ -894,6 +972,8 @@ internal enum L10n {
internal static let unknownVideoStreamInfo = L10n.tr("Localizable", "unknownVideoStreamInfo", fallback: "The video stream information is unknown")
/// Unplayed
internal static let unplayed = L10n.tr("Localizable", "unplayed", fallback: "Unplayed")
/// You have unsaved changes. Are you sure you want to discard them?
internal static let unsavedChangesMessage = L10n.tr("Localizable", "unsavedChangesMessage", fallback: "You have unsaved changes. Are you sure you want to discard them?")
/// URL
internal static let url = L10n.tr("Localizable", "url", fallback: "URL")
/// Override Transcoding Profile
@ -934,6 +1014,8 @@ internal enum L10n {
internal static let videoRangeTypeNotSupported = L10n.tr("Localizable", "videoRangeTypeNotSupported", fallback: "The video range type is not supported")
/// TranscodeReason - Video Resolution Not Supported
internal static let videoResolutionNotSupported = L10n.tr("Localizable", "videoResolutionNotSupported", fallback: "The video resolution is not supported")
/// Weekly
internal static let weekly = L10n.tr("Localizable", "weekly", fallback: "Weekly")
/// Who's watching?
internal static let whosWatching = L10n.tr("Localizable", "WhosWatching", fallback: "Who's watching?")
/// WIP

View File

@ -100,7 +100,7 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
}
await MainActor.run {
self?.backgroundStates.remove(.gettingDevices)
let _ = self?.backgroundStates.remove(.gettingDevices)
}
}
.asAnyCancellable()
@ -129,7 +129,7 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
}
await MainActor.run {
self?.backgroundStates.remove(.settingCustomName)
let _ = self?.backgroundStates.remove(.settingCustomName)
}
}
.asAnyCancellable()
@ -157,7 +157,7 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
}
await MainActor.run {
self?.backgroundStates.remove(.deletingDevices)
let _ = self?.backgroundStates.remove(.deletingDevices)
}
}
.asAnyCancellable()
@ -203,7 +203,7 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
let request = Paths.updateDeviceOptions(id: id, DeviceOptionsDto(customName: newName))
try await userSession.client.send(request)
if let device = self.devices[id]?.value {
if let _ = devices[id]?.value {
await MainActor.run {
self.devices[id]?.value?.name = newName
}
@ -222,7 +222,7 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
try await userSession.client.send(request)
await MainActor.run {
self.devices.removeValue(forKey: id)
let _ = self.devices.removeValue(forKey: id)
}
}

View File

@ -15,7 +15,7 @@ import SwiftUI
// TODO: do something for errors from restart/shutdown
// - toast?
final class ScheduledTasksViewModel: ViewModel, Stateful {
final class ServerTasksViewModel: ViewModel, Stateful {
// MARK: - Action

View File

@ -9,38 +9,76 @@
import Combine
import Foundation
import JellyfinAPI
import OrderedCollections
// TODO: refactor with socket implementation
// TODO: edit triggers
// TODO: for trigger updating, could temp set new triggers
// and set back on failure
final class ServerTaskObserver: ViewModel, Stateful, Identifiable {
final class ServerTaskObserver: ViewModel, Stateful, Eventful, Identifiable {
// MARK: Event
enum Event {
case error(JellyfinAPIError)
}
enum BackgroundState {
case updatingTriggers
}
// MARK: Action
enum Action: Equatable {
case start
case stop
case stopObserving
case addTrigger(TaskTriggerInfo)
case removeTrigger(TaskTriggerInfo)
}
// MARK: State
enum State: Hashable {
case error(JellyfinAPIError)
case initial
case running
}
// MARK: Published Values
@Published
final var backgroundStates: OrderedSet<BackgroundState> = []
@Published
final var state: State = .initial
@Published
private(set) var task: TaskInfo
// MARK: Cancellable Tasks
private var progressCancellable: AnyCancellable?
private var cancelCancellable: AnyCancellable?
// MARK: Initialize from TaskId
var id: String? { task.id }
init(task: TaskInfo) {
self.task = task
}
// MARK: Event Variables
private var eventSubject: PassthroughSubject<Event, Never> = .init()
var events: AnyPublisher<Event, Never> {
eventSubject
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
// MARK: Respond to Action
func respond(to action: Action) -> State {
switch action {
case .start:
@ -58,6 +96,7 @@ final class ServerTaskObserver: ViewModel, Stateful, Identifiable {
} catch {
await MainActor.run {
self.state = .error(.init(error.localizedDescription))
self.eventSubject.send(.error(.init(error.localizedDescription)))
}
}
}
@ -78,6 +117,7 @@ final class ServerTaskObserver: ViewModel, Stateful, Identifiable {
} catch {
await MainActor.run {
self.state = .error(.init(error.localizedDescription))
self.eventSubject.send(.error(.init(error.localizedDescription)))
}
}
}
@ -89,9 +129,65 @@ final class ServerTaskObserver: ViewModel, Stateful, Identifiable {
cancelCancellable?.cancel()
return .initial
case let .addTrigger(trigger):
progressCancellable?.cancel()
cancelCancellable?.cancel()
cancelCancellable = Task {
let updatedTriggers = (task.triggers ?? [])
.appending(trigger)
await MainActor.run {
_ = self.backgroundStates.append(.updatingTriggers)
}
do {
try await updateTriggers(updatedTriggers)
} catch {
await MainActor.run {
self.eventSubject.send(.error(.init(error.localizedDescription)))
}
}
await MainActor.run {
_ = self.backgroundStates.remove(.updatingTriggers)
}
}
.asAnyCancellable()
return .running
case let .removeTrigger(trigger):
progressCancellable?.cancel()
cancelCancellable?.cancel()
cancelCancellable = Task {
var updatedTriggers = (task.triggers ?? [])
updatedTriggers.removeAll { $0 == trigger }
await MainActor.run {
_ = self.backgroundStates.append(.updatingTriggers)
}
do {
try await updateTriggers(updatedTriggers)
} catch {
await MainActor.run {
self.eventSubject.send(.error(.init(error.localizedDescription)))
}
}
await MainActor.run {
_ = self.backgroundStates.remove(.updatingTriggers)
}
}
.asAnyCancellable()
return .running
}
}
// MARK: Start Task
private func start() async throws {
guard let id = task.id else { return }
@ -101,6 +197,8 @@ final class ServerTaskObserver: ViewModel, Stateful, Identifiable {
try await pollTaskProgress(id: id)
}
// MARK: Poll Task Progress
private func pollTaskProgress(id: String) async throws {
while true {
let request = Paths.getTask(taskID: id)
@ -118,10 +216,26 @@ final class ServerTaskObserver: ViewModel, Stateful, Identifiable {
}
}
// MARK: Stop Task
private func stop() async throws {
guard let id = task.id else { return }
let request = Paths.stopTask(taskID: id)
try await userSession.client.send(request)
try await pollTaskProgress(id: id)
}
// MARK: Update Triggers
private func updateTriggers(_ updatedTriggers: [TaskTriggerInfo]) async throws {
guard let id = task.id else { return }
let updateRequest = Paths.updateTask(taskID: id, updatedTriggers)
try await userSession.client.send(updateRequest)
await MainActor.run {
self.task.triggers = updatedTriggers
}
}
}

View File

@ -24,8 +24,8 @@
4E16FD582C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; };
4E17498E2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */; };
4E17498F2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */; };
4E182C9C2C94993200FBEFD5 /* ScheduledTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ScheduledTasksView.swift */; };
4E182C9F2C94A1E000FBEFD5 /* ScheduledTaskButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ScheduledTaskButton.swift */; };
4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */; };
4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */; };
4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */; };
4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; };
4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; };
@ -43,6 +43,19 @@
4E2AC4D42C6C4C1200DD600D /* OrderedSectionSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */; };
4E2AC4D62C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */; };
4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */; };
4E35CE5C2CBED3F300DBD886 /* TimeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE562CBED3F300DBD886 /* TimeRow.swift */; };
4E35CE5D2CBED3F300DBD886 /* TriggerTypeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE572CBED3F300DBD886 /* TriggerTypeRow.swift */; };
4E35CE5E2CBED3F300DBD886 /* AddTaskTriggerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE5A2CBED3F300DBD886 /* AddTaskTriggerView.swift */; };
4E35CE5F2CBED3F300DBD886 /* IntervalRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE542CBED3F300DBD886 /* IntervalRow.swift */; };
4E35CE602CBED3F300DBD886 /* DayOfWeekRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE532CBED3F300DBD886 /* DayOfWeekRow.swift */; };
4E35CE612CBED3F300DBD886 /* TimeLimitSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE552CBED3F300DBD886 /* TimeLimitSection.swift */; };
4E35CE642CBED69600DBD886 /* TaskTriggerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE632CBED69600DBD886 /* TaskTriggerType.swift */; };
4E35CE662CBED8B600DBD886 /* ServerTicks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE652CBED8B300DBD886 /* ServerTicks.swift */; };
4E35CE672CBED8B600DBD886 /* ServerTicks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE652CBED8B300DBD886 /* ServerTicks.swift */; };
4E35CE692CBED95F00DBD886 /* DayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */; };
4E35CE6A2CBED95F00DBD886 /* DayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */; };
4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; };
4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; };
4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; };
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
4E63B9FA2C8A5BEF00C25378 /* UserDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */; };
@ -57,6 +70,12 @@
4E762AAF2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; };
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; };
4E8B34EB2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; };
4E90F7642CC72B1F00417C31 /* LastRunSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F75B2CC72B1F00417C31 /* LastRunSection.swift */; };
4E90F7652CC72B1F00417C31 /* EditServerTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F7612CC72B1F00417C31 /* EditServerTaskView.swift */; };
4E90F7662CC72B1F00417C31 /* LastErrorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F75A2CC72B1F00417C31 /* LastErrorSection.swift */; };
4E90F7672CC72B1F00417C31 /* TriggerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F75F2CC72B1F00417C31 /* TriggerRow.swift */; };
4E90F7682CC72B1F00417C31 /* TriggersSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F75D2CC72B1F00417C31 /* TriggersSection.swift */; };
4E90F76A2CC72B1F00417C31 /* DetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F7592CC72B1F00417C31 /* DetailsSection.swift */; };
4E9A24E62C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24E52C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift */; };
4E9A24E82C82B6190023DA83 /* CustomProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */; };
4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; };
@ -64,7 +83,7 @@
4E9A24ED2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */; };
4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; };
4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; };
4EB1A8CC2C9B1BA200F43898 /* ServerTaskButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* ServerTaskButton.swift */; };
4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */; };
4EB1A8CE2C9B2D0800F43898 /* ActiveSessionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */; };
4EB4ECE32CBEFC4D002FF2FC /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */; };
4EB4ECE42CBEFC4D002FF2FC /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */; };
@ -87,7 +106,7 @@
4ECDAA9F2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; };
4EDBDCD12CBDD6590033D347 /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */; };
4EDBDCD22CBDD6590033D347 /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */; };
4EE141692C8BABDF0045B661 /* ProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ProgressSection.swift */; };
4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */; };
4EED874A2CBF824B002354D2 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87462CBF824B002354D2 /* DeviceRow.swift */; };
4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; };
4EED87502CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; };
@ -359,6 +378,7 @@
E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */; };
E1194F4E2BEABA9100888DB6 /* NavigationBarCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1194F4D2BEABA9100888DB6 /* NavigationBarCloseButton.swift */; };
E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1194F4F2BEB1E3000888DB6 /* StoredValues+Temp.swift */; };
E119696A2CC99EA9001A58BE /* ServerTaskProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11969692CC99EA9001A58BE /* ServerTaskProgressSection.swift */; };
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
E11BDF772B8513B40045C54A /* ItemGenre.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11BDF762B8513B40045C54A /* ItemGenre.swift */; };
@ -979,7 +999,6 @@
E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EA9F6928F8A79E00BEC442 /* VideoPlayerManager.swift */; };
E1EBCB42278BD174009FE6E9 /* TruncatedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */; };
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */; };
E1ED7FD62CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FD52CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift */; };
E1ED7FD82CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FD72CA8AF7400ACB6E3 /* ServerTaskObserver.swift */; };
E1ED7FD92CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FD72CA8AF7400ACB6E3 /* ServerTaskObserver.swift */; };
E1ED7FDB2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */; };
@ -1048,8 +1067,8 @@
4E16FD522C01840C00110147 /* LetterPickerBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerBar.swift; sourceTree = "<group>"; };
4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerOrientation.swift; sourceTree = "<group>"; };
4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = "<group>"; };
4E182C9B2C94993200FBEFD5 /* ScheduledTasksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduledTasksView.swift; sourceTree = "<group>"; };
4E182C9E2C94A1E000FBEFD5 /* ScheduledTaskButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduledTaskButton.swift; sourceTree = "<group>"; };
4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksView.swift; sourceTree = "<group>"; };
4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskRow.swift; sourceTree = "<group>"; };
4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeSettingsCoordinator.swift; sourceTree = "<group>"; };
4E2182E42CAF67EF0094806B /* PlayMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayMethod.swift; sourceTree = "<group>"; };
4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileAction.swift; sourceTree = "<group>"; };
@ -1061,6 +1080,16 @@
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>"; };
4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackQualitySettingsView.swift; sourceTree = "<group>"; };
4E35CE532CBED3F300DBD886 /* DayOfWeekRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeekRow.swift; sourceTree = "<group>"; };
4E35CE542CBED3F300DBD886 /* IntervalRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalRow.swift; sourceTree = "<group>"; };
4E35CE552CBED3F300DBD886 /* TimeLimitSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeLimitSection.swift; sourceTree = "<group>"; };
4E35CE562CBED3F300DBD886 /* TimeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeRow.swift; sourceTree = "<group>"; };
4E35CE572CBED3F300DBD886 /* TriggerTypeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerTypeRow.swift; sourceTree = "<group>"; };
4E35CE5A2CBED3F300DBD886 /* AddTaskTriggerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTaskTriggerView.swift; sourceTree = "<group>"; };
4E35CE632CBED69600DBD886 /* TaskTriggerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskTriggerType.swift; sourceTree = "<group>"; };
4E35CE652CBED8B300DBD886 /* ServerTicks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTicks.swift; sourceTree = "<group>"; };
4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeek.swift; sourceTree = "<group>"; };
4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = "<group>"; };
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDashboardView.swift; sourceTree = "<group>"; };
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = "<group>"; };
@ -1071,13 +1100,19 @@
4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = "<group>"; };
4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = "<group>"; };
4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; };
4E90F7592CC72B1F00417C31 /* DetailsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsSection.swift; sourceTree = "<group>"; };
4E90F75A2CC72B1F00417C31 /* LastErrorSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastErrorSection.swift; sourceTree = "<group>"; };
4E90F75B2CC72B1F00417C31 /* LastRunSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastRunSection.swift; sourceTree = "<group>"; };
4E90F75D2CC72B1F00417C31 /* TriggersSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggersSection.swift; sourceTree = "<group>"; };
4E90F75F2CC72B1F00417C31 /* TriggerRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerRow.swift; sourceTree = "<group>"; };
4E90F7612CC72B1F00417C31 /* EditServerTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServerTaskView.swift; sourceTree = "<group>"; };
4E9A24E52C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileSettingsView.swift; sourceTree = "<group>"; };
4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProfileButton.swift; sourceTree = "<group>"; };
4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileCoordinator.swift; sourceTree = "<group>"; };
4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = "<group>"; };
4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = "<group>"; };
4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = "<group>"; };
4EB1A8CB2C9B1B9700F43898 /* ServerTaskButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskButton.swift; sourceTree = "<group>"; };
4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = "<group>"; };
4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionRow.swift; sourceTree = "<group>"; };
4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = "<group>"; };
4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronAlertButton.swift; sourceTree = "<group>"; };
@ -1092,7 +1127,7 @@
4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = "<group>"; };
4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = "<group>"; };
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = "<group>"; };
4EE141682C8BABDF0045B661 /* ProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressSection.swift; sourceTree = "<group>"; };
4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.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>"; };
4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = "<group>"; };
@ -1308,6 +1343,7 @@
E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundParallaxHeaderModifier.swift; sourceTree = "<group>"; };
E1194F4D2BEABA9100888DB6 /* NavigationBarCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarCloseButton.swift; sourceTree = "<group>"; };
E1194F4F2BEB1E3000888DB6 /* StoredValues+Temp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredValues+Temp.swift"; sourceTree = "<group>"; };
E11969692CC99EA9001A58BE /* ServerTaskProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskProgressSection.swift; sourceTree = "<group>"; };
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
E11BDF762B8513B40045C54A /* ItemGenre.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemGenre.swift; sourceTree = "<group>"; };
E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedCaseIterable.swift; sourceTree = "<group>"; };
@ -1697,7 +1733,6 @@
E1EA9F6928F8A79E00BEC442 /* VideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerManager.swift; sourceTree = "<group>"; };
E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; };
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewView.swift; sourceTree = "<group>"; };
E1ED7FD52CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditScheduledTaskView.swift; sourceTree = "<group>"; };
E1ED7FD72CA8AF7400ACB6E3 /* ServerTaskObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskObserver.swift; sourceTree = "<group>"; };
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStateInfo.swift; sourceTree = "<group>"; };
E1ED7FDD2CAA641F00ACB6E3 /* ListTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTitleSection.swift; sourceTree = "<group>"; };
@ -1881,20 +1916,20 @@
path = Components;
sourceTree = "<group>";
};
4E182C9A2C94991800FBEFD5 /* ScheduledTasksView */ = {
4E182C9A2C94991800FBEFD5 /* ServerTasksView */ = {
isa = PBXGroup;
children = (
4E182C9D2C94A01600FBEFD5 /* Components */,
4E182C9B2C94993200FBEFD5 /* ScheduledTasksView.swift */,
4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */,
);
path = ScheduledTasksView;
path = ServerTasksView;
sourceTree = "<group>";
};
4E182C9D2C94A01600FBEFD5 /* Components */ = {
isa = PBXGroup;
children = (
4E182C9E2C94A1E000FBEFD5 /* ScheduledTaskButton.swift */,
4EB1A8CB2C9B1B9700F43898 /* ServerTaskButton.swift */,
4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */,
4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */,
);
path = Components;
sourceTree = "<group>";
@ -1910,6 +1945,35 @@
path = MediaComponents;
sourceTree = "<group>";
};
4E35CE592CBED3F300DBD886 /* Components */ = {
isa = PBXGroup;
children = (
4E35CE532CBED3F300DBD886 /* DayOfWeekRow.swift */,
4E35CE542CBED3F300DBD886 /* IntervalRow.swift */,
4E35CE552CBED3F300DBD886 /* TimeLimitSection.swift */,
4E35CE562CBED3F300DBD886 /* TimeRow.swift */,
4E35CE572CBED3F300DBD886 /* TriggerTypeRow.swift */,
);
path = Components;
sourceTree = "<group>";
};
4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */ = {
isa = PBXGroup;
children = (
4E35CE592CBED3F300DBD886 /* Components */,
4E35CE5A2CBED3F300DBD886 /* AddTaskTriggerView.swift */,
);
path = AddTaskTriggerView;
sourceTree = "<group>";
};
4E35CE622CBED3FF00DBD886 /* ServerLogsView */ = {
isa = PBXGroup;
children = (
E1ED7FDF2CAA685900ACB6E3 /* ServerLogsView.swift */,
);
path = ServerLogsView;
sourceTree = "<group>";
};
4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */ = {
isa = PBXGroup;
children = (
@ -1922,14 +1986,15 @@
4E63B9F52C8A5BEF00C25378 /* UserDashboardView */ = {
isa = PBXGroup;
children = (
E1DE64902CC6F06C00E423B6 /* Components */,
4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */,
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */,
4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */,
E1DE64902CC6F06C00E423B6 /* Components */,
4E10C80F2CC030B20012CC9F /* DeviceDetailsView */,
4EED87492CBF824B002354D2 /* DevicesView */,
E1ED7FD52CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift */,
4E182C9A2C94991800FBEFD5 /* ScheduledTasksView */,
E1ED7FDF2CAA685900ACB6E3 /* ServerLogsView.swift */,
4E90F7622CC72B1F00417C31 /* EditServerTaskView */,
4E182C9A2C94991800FBEFD5 /* ServerTasksView */,
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */,
);
path = UserDashboardView;
@ -1995,6 +2060,36 @@
path = ActiveSessionDetailView;
sourceTree = "<group>";
};
4E90F75E2CC72B1F00417C31 /* Sections */ = {
isa = PBXGroup;
children = (
4E90F7592CC72B1F00417C31 /* DetailsSection.swift */,
4E90F75A2CC72B1F00417C31 /* LastErrorSection.swift */,
4E90F75B2CC72B1F00417C31 /* LastRunSection.swift */,
E11969692CC99EA9001A58BE /* ServerTaskProgressSection.swift */,
4E90F75D2CC72B1F00417C31 /* TriggersSection.swift */,
);
path = Sections;
sourceTree = "<group>";
};
4E90F7602CC72B1F00417C31 /* Components */ = {
isa = PBXGroup;
children = (
4E90F75E2CC72B1F00417C31 /* Sections */,
4E90F75F2CC72B1F00417C31 /* TriggerRow.swift */,
);
path = Components;
sourceTree = "<group>";
};
4E90F7622CC72B1F00417C31 /* EditServerTaskView */ = {
isa = PBXGroup;
children = (
4E90F7602CC72B1F00417C31 /* Components */,
4E90F7612CC72B1F00417C31 /* EditServerTaskView.swift */,
);
path = EditServerTaskView;
sourceTree = "<group>";
};
4E9A24E32C82B4700023DA83 /* CustomDeviceProfileSettingsView */ = {
isa = PBXGroup;
children = (
@ -2025,8 +2120,8 @@
4EB1A8D02C9B2FB600F43898 /* Components */ = {
isa = PBXGroup;
children = (
4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */,
4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */,
4EE141682C8BABDF0045B661 /* ProgressSection.swift */,
);
path = Components;
sourceTree = "<group>";
@ -3640,13 +3735,12 @@
E1D37F5B2B9CF02600343D2B /* BaseItemDto */,
E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */,
E1002B632793CEE700E47059 /* ChapterInfo.swift */,
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */,
E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */,
4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */,
4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */,
4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */,
4E12F9152CBE9615006C217E /* DeviceType.swift */,
4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */,
4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */,
E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */,
E1722DB029491C3900CC0239 /* ImageBlurHashes.swift */,
E1D842902933F87500D1041A /* ItemFields.swift */,
@ -3656,11 +3750,16 @@
E1F5F9B12BA0200500BA5014 /* MediaSourceInfo */,
E122A9122788EAAD0060FA63 /* MediaStream.swift */,
E1AD105E26D9ADDD003E4A08 /* NameGuidPair.swift */,
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */,
4E2182E42CAF67EF0094806B /* PlayMethod.swift */,
4E35CE652CBED8B300DBD886 /* ServerTicks.swift */,
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */,
E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */,
E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */,
E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */,
4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */,
4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */,
4E35CE632CBED69600DBD886 /* TaskTriggerType.swift */,
4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */,
E1CB757B2C80F00D00217C76 /* TranscodingProfile.swift */,
E18CE0B128A229E70092E7F1 /* UserDto.swift */,
@ -4428,6 +4527,7 @@
E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */,
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
E122A9142788EAAD0060FA63 /* MediaStream.swift in Sources */,
4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */,
4E2AC4D62C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift in Sources */,
E102314E2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */,
E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */,
@ -4628,6 +4728,7 @@
E18A17F2298C68BB00C22F62 /* MainOverlay.swift in Sources */,
E1763A6A2BF3D177004DF6AB /* PublicUserRow.swift in Sources */,
E1E6C44B29AED2B70064123F /* HorizontalAlignment.swift in Sources */,
4E35CE672CBED8B600DBD886 /* ServerTicks.swift in Sources */,
E193D549271941CC00900D82 /* UserSignInView.swift in Sources */,
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
E148128628C15475003B8787 /* SortOrder+ItemSortOrder.swift in Sources */,
@ -4686,6 +4787,7 @@
E1153D962BBA3E2F00424D36 /* EpisodeHStack.swift in Sources */,
E193D5512719432400900D82 /* ServerConnectionViewModel.swift in Sources */,
E1B5861329E32EEF00E45D6E /* Sequence.swift in Sources */,
4E35CE6A2CBED95F00DBD886 /* DayOfWeek.swift in Sources */,
C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */,
E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */,
E113133B28BEB71D00930F75 /* FilterViewModel.swift in Sources */,
@ -4924,6 +5026,7 @@
E1BDF31729525F0400CC0294 /* AdvancedActionButton.swift in Sources */,
E1ED91152B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
E119696A2CC99EA9001A58BE /* ServerTaskProgressSection.swift in Sources */,
E1BAFE102BE921270069C4D7 /* SwiftfinApp+ValueObservation.swift in Sources */,
E1ED7FDE2CAA641F00ACB6E3 /* ListTitleSection.swift in Sources */,
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
@ -4932,6 +5035,7 @@
4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */,
E1DD55372B6EE533007501C0 /* Task.swift in Sources */,
E1ED7FE02CAA685900ACB6E3 /* ServerLogsView.swift in Sources */,
4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */,
E1194F4E2BEABA9100888DB6 /* NavigationBarCloseButton.swift in Sources */,
E113133428BE988200930F75 /* NavigationBarFilterDrawer.swift in Sources */,
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
@ -4939,6 +5043,12 @@
E11E0E8C2BF7E76F007676DD /* DataCache.swift in Sources */,
E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
4E90F7642CC72B1F00417C31 /* LastRunSection.swift in Sources */,
4E90F7652CC72B1F00417C31 /* EditServerTaskView.swift in Sources */,
4E90F7662CC72B1F00417C31 /* LastErrorSection.swift in Sources */,
4E90F7672CC72B1F00417C31 /* TriggerRow.swift in Sources */,
4E90F7682CC72B1F00417C31 /* TriggersSection.swift in Sources */,
4E90F76A2CC72B1F00417C31 /* DetailsSection.swift in Sources */,
E129428828F0831F00796AC6 /* SplitTimestamp.swift in Sources */,
C46DD8E72A8FA77F0046A504 /* LiveBottomBarView.swift in Sources */,
E11CEB8D28999B4A003E74C7 /* Font.swift in Sources */,
@ -4998,7 +5108,7 @@
E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */,
E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */,
E1549666296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */,
4EE141692C8BABDF0045B661 /* ProgressSection.swift in Sources */,
4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */,
E1A1528528FD191A00600579 /* TextPair.swift in Sources */,
6334175D287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift in Sources */,
4EED874A2CBF824B002354D2 /* DeviceRow.swift in Sources */,
@ -5044,9 +5154,15 @@
E18A8E8028D6083700333B9A /* MediaSourceInfo+ItemVideoPlayerViewModel.swift in Sources */,
E18E01DC288747230022598C /* iPadOSCinematicScrollView.swift in Sources */,
E18E01E2288747230022598C /* EpisodeItemView.swift in Sources */,
4E35CE5C2CBED3F300DBD886 /* TimeRow.swift in Sources */,
4E35CE5D2CBED3F300DBD886 /* TriggerTypeRow.swift in Sources */,
4E35CE5E2CBED3F300DBD886 /* AddTaskTriggerView.swift in Sources */,
4E35CE5F2CBED3F300DBD886 /* IntervalRow.swift in Sources */,
4E35CE602CBED3F300DBD886 /* DayOfWeekRow.swift in Sources */,
4E35CE612CBED3F300DBD886 /* TimeLimitSection.swift in Sources */,
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
E14EA1602BF6FF8900DE757A /* UserProfileImagePicker.swift in Sources */,
4E182C9C2C94993200FBEFD5 /* ScheduledTasksView.swift in Sources */,
4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */,
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */,
E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */,
@ -5057,6 +5173,7 @@
E10B1ECA2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */,
E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */,
4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */,
4E35CE692CBED95F00DBD886 /* DayOfWeek.swift in Sources */,
E18E01E3288747230022598C /* CompactPortraitScrollView.swift in Sources */,
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
@ -5076,6 +5193,7 @@
E113133A28BEB71D00930F75 /* FilterViewModel.swift in Sources */,
E1E6C44C29AED2BE0064123F /* HorizontalAlignment.swift in Sources */,
E1A1528D28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift in Sources */,
4E35CE642CBED69600DBD886 /* TaskTriggerType.swift in Sources */,
E18E01EE288747230022598C /* AboutView.swift in Sources */,
62E632E0267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */,
E1B33EB028EA890D0073B0FD /* Equatable.swift in Sources */,
@ -5135,7 +5253,7 @@
E1401CA72938140300E8B599 /* PrimaryAppIcon.swift in Sources */,
E1937A3E288F0D3D00CB80AA /* UIScreen.swift in Sources */,
E10B1EBE2BD9AD5C00A92EAF /* V1ServerModel.swift in Sources */,
4EB1A8CC2C9B1BA200F43898 /* ServerTaskButton.swift in Sources */,
4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */,
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
E10B1EB62BD98C6600A92EAF /* AddUserRow.swift in Sources */,
E1CB75802C80F28F00217C76 /* SubtitleProfile.swift in Sources */,
@ -5154,7 +5272,6 @@
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
E1721FAE28FB801C00762992 /* SmallPlaybackButtons.swift in Sources */,
E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */,
E1ED7FD62CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift in Sources */,
4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */,
E1545BD82BDC55C300D9578F /* ResetUserPasswordView.swift in Sources */,
E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */,
@ -5175,7 +5292,7 @@
E18E01F1288747230022598C /* PlayButton.swift in Sources */,
E129429028F0BDC300796AC6 /* TimeStampType.swift in Sources */,
E1F5CF092CB0A04500607465 /* Text.swift in Sources */,
4E182C9F2C94A1E000FBEFD5 /* ScheduledTaskButton.swift in Sources */,
4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */,
E1B490442967E26300D3EDCE /* PersistentLogHandler.swift in Sources */,
E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */,
E19D41AC2BF288110082B8B2 /* ServerCheckView.swift in Sources */,
@ -5221,6 +5338,7 @@
E1A3E4C72BB74E50005C59F8 /* EpisodeCard.swift in Sources */,
E1153DB42BBA80FB00424D36 /* EmptyCard.swift in Sources */,
4E63B9FA2C8A5BEF00C25378 /* UserDashboardView.swift in Sources */,
4E35CE662CBED8B600DBD886 /* ServerTicks.swift in Sources */,
E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */,
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */,
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,

View File

@ -0,0 +1,134 @@
//
// 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 AddTaskTriggerView: View {
@Environment(\.dismiss)
private var dismiss
@ObservedObject
var observer: ServerTaskObserver
@State
private var isPresentingNotSaved = false
@State
private var taskTriggerInfo: TaskTriggerInfo
static let defaultTimeOfDayTicks = 0
static let defaultDayOfWeek: DayOfWeek = .sunday
static let defaultIntervalTicks = 36_000_000_000
private let emptyTaskTriggerInfo: TaskTriggerInfo
private var hasUnsavedChanges: Bool {
taskTriggerInfo != emptyTaskTriggerInfo
}
private var isDuplicate: Bool {
observer.task.triggers?.contains(where: { $0 == taskTriggerInfo }) ?? false
}
// MARK: - Init
init(observer: ServerTaskObserver) {
self.observer = observer
let newTrigger = TaskTriggerInfo(
dayOfWeek: nil,
intervalTicks: nil,
maxRuntimeTicks: nil,
timeOfDayTicks: nil,
type: TaskTriggerType.startup.rawValue
)
_taskTriggerInfo = State(initialValue: newTrigger)
self.emptyTaskTriggerInfo = newTrigger
}
// MARK: - View for TaskTriggerType.daily
@ViewBuilder
private var dailyView: some View {
TimeRow(taskTriggerInfo: $taskTriggerInfo)
}
// MARK: - View for TaskTriggerType.weekly
@ViewBuilder
private var weeklyView: some View {
DayOfWeekRow(taskTriggerInfo: $taskTriggerInfo)
TimeRow(taskTriggerInfo: $taskTriggerInfo)
}
// MARK: - View for TaskTriggerType.interval
@ViewBuilder
private var intervalView: some View {
IntervalRow(taskTriggerInfo: $taskTriggerInfo)
}
// MARK: - Body
var body: some View {
Form {
Section {
TriggerTypeRow(taskTriggerInfo: $taskTriggerInfo)
if let taskType = taskTriggerInfo.type {
if taskType == TaskTriggerType.daily.rawValue {
dailyView
} else if taskType == TaskTriggerType.weekly.rawValue {
weeklyView
} else if taskType == TaskTriggerType.interval.rawValue {
intervalView
}
}
} footer: {
if isDuplicate {
Label(L10n.triggerAlreadyExists, systemImage: "exclamationmark.circle.fill")
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
}
}
TimeLimitSection(taskTriggerInfo: $taskTriggerInfo)
}
.animation(.linear(duration: 0.2), value: isDuplicate)
.animation(.linear(duration: 0.2), value: taskTriggerInfo.type)
.interactiveDismissDisabled(true)
.navigationTitle(L10n.addTrigger)
.navigationBarTitleDisplayMode(.inline)
.navigationBarCloseButton {
if hasUnsavedChanges {
isPresentingNotSaved = true
} else {
dismiss()
}
}
.topBarTrailing {
Button(L10n.save) {
UIDevice.impact(.light)
observer.send(.addTrigger(taskTriggerInfo))
dismiss()
}
.buttonStyle(.toolbarPill)
.disabled(isDuplicate)
}
.alert(L10n.unsavedChangesMessage, isPresented: $isPresentingNotSaved) {
Button(L10n.close, role: .destructive) {
dismiss()
}
Button(L10n.cancel, role: .cancel) {
isPresentingNotSaved = false
}
}
}
}

View File

@ -0,0 +1,36 @@
//
// 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
extension AddTaskTriggerView {
struct DayOfWeekRow: View {
@Binding
var taskTriggerInfo: TaskTriggerInfo
// MARK: - Body
var body: some View {
Picker(
L10n.dayOfWeek,
selection: Binding(
get: { taskTriggerInfo.dayOfWeek ?? defaultDayOfWeek },
set: { taskTriggerInfo.dayOfWeek = $0 }
)
) {
ForEach(DayOfWeek.allCases, id: \.self) { day in
Text(day.displayTitle ?? L10n.unknown)
.tag(day)
}
}
}
}
}

View File

@ -0,0 +1,60 @@
//
// 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
extension AddTaskTriggerView {
struct IntervalRow: View {
@Binding
private var taskTriggerInfo: TaskTriggerInfo
@State
private var tempInterval: Int?
// MARK: - Init
init(taskTriggerInfo: Binding<TaskTriggerInfo>) {
self._taskTriggerInfo = taskTriggerInfo
_tempInterval = State(initialValue: Int(ServerTicks(taskTriggerInfo.wrappedValue.intervalTicks).minutes))
}
// MARK: - Body
var body: some View {
ChevronAlertButton(
L10n.every,
subtitle: ServerTicks(
taskTriggerInfo.intervalTicks
).seconds.formatted(.hourMinute),
description: L10n.taskTriggerInterval
) {
TextField(
L10n.minutes,
value: $tempInterval,
format: .number
)
.keyboardType(.numberPad)
} onSave: {
if tempInterval != nil && tempInterval != 0 {
taskTriggerInfo.intervalTicks = ServerTicks(minutes: tempInterval).ticks
} else {
taskTriggerInfo.intervalTicks = nil
}
} onCancel: {
if let intervalTicks = taskTriggerInfo.intervalTicks {
tempInterval = Int(ServerTicks(intervalTicks).minutes)
} else {
tempInterval = nil
}
}
}
}
}

View File

@ -0,0 +1,70 @@
//
// 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
extension AddTaskTriggerView {
struct TimeLimitSection: View {
@Binding
private var taskTriggerInfo: TaskTriggerInfo
@State
private var tempTimeLimit: Int?
// MARK: - Init
init(taskTriggerInfo: Binding<TaskTriggerInfo>) {
self._taskTriggerInfo = taskTriggerInfo
_tempTimeLimit = State(initialValue: Int(ServerTicks(taskTriggerInfo.wrappedValue.maxRuntimeTicks).hours))
}
// MARK: - Body
var body: some View {
Section {
ChevronAlertButton(
L10n.timeLimit,
subtitle: subtitleString,
description: L10n.taskTriggerTimeLimit
) {
TextField(
L10n.hours,
value: $tempTimeLimit,
format: .number
)
.keyboardType(.numberPad)
} onSave: {
if tempTimeLimit != nil && tempTimeLimit != 0 {
taskTriggerInfo.maxRuntimeTicks = ServerTicks(hours: tempTimeLimit).ticks
} else {
taskTriggerInfo.maxRuntimeTicks = nil
}
} onCancel: {
if let maxRuntimeTicks = taskTriggerInfo.maxRuntimeTicks {
tempTimeLimit = Int(ServerTicks(maxRuntimeTicks).hours)
} else {
tempTimeLimit = nil
}
}
}
}
// MARK: - Create Subtitle String
private var subtitleString: String {
if let maxRuntimeTicks = taskTriggerInfo.maxRuntimeTicks {
ServerTicks(maxRuntimeTicks).seconds.formatted(.hourMinute)
} else {
L10n.none
}
}
}
}

View File

@ -0,0 +1,36 @@
//
// 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
extension AddTaskTriggerView {
struct TimeRow: View {
@Binding
var taskTriggerInfo: TaskTriggerInfo
var body: some View {
DatePicker(
L10n.time,
selection: Binding<Date>(
get: {
ServerTicks(
taskTriggerInfo.timeOfDayTicks ?? defaultTimeOfDayTicks
).date
},
set: { date in
taskTriggerInfo.timeOfDayTicks = ServerTicks(date: date).ticks
}
),
displayedComponents: .hourAndMinute
)
}
}
}

View File

@ -0,0 +1,74 @@
//
// 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
extension AddTaskTriggerView {
struct TriggerTypeRow: View {
@Binding
var taskTriggerInfo: TaskTriggerInfo
var body: some View {
Picker(
L10n.type,
selection: Binding<TaskTriggerType?>(
get: {
if let t = taskTriggerInfo.type {
return TaskTriggerType(rawValue: t)
} else {
return nil
}
},
set: { newValue in
if taskTriggerInfo.type != newValue?.rawValue {
resetValuesForNewType(newType: newValue)
}
}
)
) {
ForEach(TaskTriggerType.allCases, id: \.self) { type in
Text(type.displayTitle)
.tag(type as TaskTriggerType?)
}
}
}
private func resetValuesForNewType(newType: TaskTriggerType?) {
taskTriggerInfo.type = newType?.rawValue
let maxRuntimeTicks = taskTriggerInfo.maxRuntimeTicks
switch newType {
case .daily:
taskTriggerInfo.timeOfDayTicks = defaultTimeOfDayTicks
taskTriggerInfo.dayOfWeek = nil
taskTriggerInfo.intervalTicks = nil
case .weekly:
taskTriggerInfo.timeOfDayTicks = defaultTimeOfDayTicks
taskTriggerInfo.dayOfWeek = defaultDayOfWeek
taskTriggerInfo.intervalTicks = nil
case .interval:
taskTriggerInfo.intervalTicks = defaultIntervalTicks
taskTriggerInfo.timeOfDayTicks = nil
taskTriggerInfo.dayOfWeek = nil
case .startup:
taskTriggerInfo.timeOfDayTicks = nil
taskTriggerInfo.dayOfWeek = nil
taskTriggerInfo.intervalTicks = nil
default:
taskTriggerInfo.timeOfDayTicks = nil
taskTriggerInfo.dayOfWeek = nil
taskTriggerInfo.intervalTicks = nil
}
taskTriggerInfo.maxRuntimeTicks = maxRuntimeTicks
}
}
}

View File

@ -1,95 +0,0 @@
//
// 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
// TODO: last run details
// - result, show error if available
// TODO: observe running status
// - stop
// - run
// - progress
// TODO: triggers
struct EditScheduledTaskView: View {
@CurrentDate
private var currentDate: Date
@ObservedObject
var observer: ServerTaskObserver
var body: some View {
List {
ListTitleSection(
observer.task.name ?? L10n.unknown,
description: observer.task.description
)
if let category = observer.task.category {
TextPairView(
leading: L10n.category,
trailing: category
)
}
if let lastEndTime = observer.task.lastExecutionResult?.endTimeUtc {
TextPairView(
L10n.lastRun,
value: Text("\(lastEndTime, format: .relative(presentation: .numeric, unitsStyle: .narrow))")
)
.id(currentDate)
.monospacedDigit()
if let lastStartTime = observer.task.lastExecutionResult?.startTimeUtc {
TextPairView(
L10n.runtime,
value: Text(
"\(lastStartTime ..< lastEndTime, format: .components(style: .narrow))"
)
)
}
}
}
.navigationTitle(L10n.task)
}
}
// TODO: remove after view done
#Preview {
NavigationView {
EditScheduledTaskView(
observer: .init(
task: TaskInfo(
category: "test",
currentProgressPercentage: nil,
description: "A test task",
id: "123",
isHidden: false,
key: "123",
lastExecutionResult: TaskResult(
endTimeUtc: Date(timeIntervalSinceNow: -10),
errorMessage: nil,
id: nil,
key: nil,
longErrorMessage: nil,
name: nil,
startTimeUtc: Date(timeIntervalSinceNow: -30),
status: .completed
),
name: "Test",
state: .running,
triggers: nil
)
)
)
.navigationBarTitleDisplayMode(.inline)
}
}

View File

@ -0,0 +1,23 @@
//
// 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
extension EditServerTaskView {
struct DetailsSection: View {
let category: String
var body: some View {
Section(L10n.details) {
TextPairView(leading: L10n.category, trailing: category)
}
}
}
}

View File

@ -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) 2024 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import SwiftUI
extension EditServerTaskView {
struct LastErrorSection: View {
let message: String
var body: some View {
Section(L10n.errorDetails) {
Text(message)
}
}
}
}

View File

@ -0,0 +1,39 @@
//
// 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
extension EditServerTaskView {
struct LastRunSection: View {
@CurrentDate
private var currentDate: Date
let status: TaskCompletionStatus
let endTime: Date
var body: some View {
Section(L10n.lastRun) {
TextPairView(
leading: L10n.status,
trailing: status.displayTitle
)
TextPairView(
L10n.executed,
value: Text("\(endTime, format: .relative(presentation: .numeric, unitsStyle: .narrow))")
)
.id(currentDate)
.monospacedDigit()
}
}
}
}

View File

@ -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
//
//
// 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
extension EditServerTaskView {
struct ProgressSection: View {
@ObservedObject
var observer: ServerTaskObserver
var body: some View {
if observer.task.state == .running || observer.task.state == .cancelling {
Section(L10n.progress) {
if let status = observer.task.state {
TextPairView(
leading: L10n.status,
trailing: status.displayTitle
)
}
if let currentProgressPercentage = observer.task.currentProgressPercentage {
TextPairView(
L10n.progress,
value: Text("\(currentProgressPercentage / 100, format: .percent.precision(.fractionLength(1)))")
)
.monospacedDigit()
}
Button {
observer.send(.stop)
} label: {
HStack {
Text(L10n.stop)
Spacer()
Image(systemName: "stop.fill")
}
}
.foregroundStyle(.red)
}
} else {
Button(L10n.run) {
observer.send(.start)
}
}
}
}
}

View File

@ -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 JellyfinAPI
import SwiftUI
extension EditServerTaskView {
struct TriggersSection: View {
@EnvironmentObject
private var router: SettingsCoordinator.Router
@ObservedObject
var observer: ServerTaskObserver
@State
private var isPresentingDeleteConfirmation: Bool = false
@State
private var selectedTrigger: TaskTriggerInfo?
var body: some View {
Section(L10n.triggers) {
if let triggers = observer.task.triggers, triggers.isNotEmpty {
ForEach(triggers, id: \.self) { trigger in
TriggerRow(taskTriggerInfo: trigger)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
selectedTrigger = trigger
isPresentingDeleteConfirmation = true
} label: {
Label(L10n.delete, systemImage: "trash")
}
.tint(.red)
}
}
} else {
Button(L10n.addTrigger) {
router.route(to: \.addServerTaskTrigger, observer)
}
}
}
.confirmationDialog(
L10n.deleteTrigger,
isPresented: $isPresentingDeleteConfirmation,
titleVisibility: .visible
) {
Button(L10n.cancel, role: .cancel) {}
Button(L10n.delete, role: .destructive) {
if let selectedTrigger {
observer.send(.removeTrigger(selectedTrigger))
}
}
} message: {
Text(L10n.deleteTriggerConfirmationMessage)
}
}
}
}

View File

@ -0,0 +1,98 @@
//
// 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 Stinsen
import SwiftUI
extension EditServerTaskView {
struct TriggerRow: View {
let taskTriggerInfo: TaskTriggerInfo
// TODO: remove after `TaskTriggerType` is provided by SDK
private var taskTriggerType: TaskTriggerType {
if let t = taskTriggerInfo.type, let type = TaskTriggerType(rawValue: t) {
return type
} else {
return .startup
}
}
// MARK: - Body
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(triggerDisplayText)
.fontWeight(.semibold)
Group {
if let maxRuntimeTicks = taskTriggerInfo.maxRuntimeTicks {
Text(
L10n.timeLimitLabelWithValue(
ServerTicks(maxRuntimeTicks)
.seconds.formatted(.hourMinute)
)
)
} else {
Text(L10n.noRuntimeLimit)
}
}
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: taskTriggerType.systemImage)
.backport
.fontWeight(.bold)
.foregroundStyle(.secondary)
}
}
// MARK: - Trigger Display Text
private var triggerDisplayText: String {
switch taskTriggerType {
case .daily:
if let timeOfDayTicks = taskTriggerInfo.timeOfDayTicks {
return L10n.itemAtItem(
taskTriggerType.displayTitle,
ServerTicks(timeOfDayTicks)
.date.formatted(date: .omitted, time: .shortened)
)
}
case .weekly:
if let dayOfWeek = taskTriggerInfo.dayOfWeek,
let timeOfDayTicks = taskTriggerInfo.timeOfDayTicks
{
return L10n.itemAtItem(
dayOfWeek.rawValue.capitalized,
ServerTicks(timeOfDayTicks)
.date.formatted(date: .omitted, time: .shortened)
)
}
case .interval:
if let intervalTicks = taskTriggerInfo.intervalTicks {
return L10n.everyInterval(
ServerTicks(intervalTicks)
.seconds.formatted(.hourMinute)
)
}
case .startup:
return taskTriggerType.displayTitle
}
return L10n.unknown
}
}
}

View File

@ -0,0 +1,94 @@
//
// 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 EditServerTaskView: View {
@EnvironmentObject
private var router: SettingsCoordinator.Router
@ObservedObject
var observer: ServerTaskObserver
// MARK: - State Variables
@State
private var isPresentingDeleteConfirmation = false
@State
private var isPresentingEventAlert = false
@State
private var error: JellyfinAPIError?
@State
private var selectedTrigger: TaskTriggerInfo?
// MARK: - Body
var body: some View {
List {
ListTitleSection(
observer.task.name ?? L10n.unknown,
description: observer.task.description
)
ProgressSection(observer: observer)
if let category = observer.task.category {
DetailsSection(category: category)
}
if let lastExecutionResult = observer.task.lastExecutionResult {
if let status = lastExecutionResult.status, let endTime = lastExecutionResult.endTimeUtc {
LastRunSection(status: status, endTime: endTime)
}
if let errorMessage = lastExecutionResult.errorMessage {
LastErrorSection(message: errorMessage)
}
}
TriggersSection(observer: observer)
}
.animation(.linear(duration: 0.2), value: observer.state)
.animation(.linear(duration: 0.1), value: observer.task.state)
.animation(.linear(duration: 0.1), value: observer.task.triggers)
.navigationTitle(L10n.task)
.topBarTrailing {
if observer.backgroundStates.contains(.updatingTriggers) {
ProgressView()
}
if let triggers = observer.task.triggers, triggers.isNotEmpty {
Button(L10n.add) {
UIDevice.impact(.light)
router.route(to: \.addServerTaskTrigger, observer)
}
.buttonStyle(.toolbarPill)
}
}
.onReceive(observer.events) { event in
switch event {
case let .error(eventError):
error = eventError
isPresentingEventAlert = true
}
}
.alert(
L10n.error,
isPresented: $isPresentingEventAlert,
presenting: error
) { _ in
} message: { error in
Text(error.localizedDescription)
}
}
}

View File

@ -24,6 +24,12 @@ struct ServerLogsView: View {
@ViewBuilder
private var contentView: some View {
List {
ListTitleSection(
L10n.logs,
description: L10n.logsDescription
) {
UIApplication.shared.open(URL(string: "https://jellyfin.org/docs/general/administration/troubleshooting")!)
}
ForEach(viewModel.logs, id: \.self) { log in
Button {
let request = Paths.getLogFile(name: log.name!)

View File

@ -10,38 +10,43 @@ import Defaults
import JellyfinAPI
import SwiftUI
extension ScheduledTasksView {
extension ServerTasksView {
struct ServerTaskButton: View {
struct DestructiveServerTask: View {
@State
private var isPresented: Bool = false
let title: String
let systemImage: String
let warningMessage: String
let isPresented: Binding<Bool>
let systemName: String
let message: String
let action: () -> Void
// MARK: - Body
var body: some View {
Button(role: .destructive) {
isPresented.wrappedValue = true
isPresented = true
} label: {
HStack {
Text(title)
.fontWeight(.semibold)
Spacer()
Image(systemName: systemImage)
Image(systemName: systemName)
.backport
.fontWeight(.bold)
}
}
.confirmationDialog(
title,
isPresented: isPresented,
titleVisibility: .hidden
isPresented: $isPresented,
titleVisibility: .visible
) {
Button(title, role: .destructive, action: action)
} message: {
Text(warningMessage)
Text(message)
}
}
}

View File

@ -10,9 +10,9 @@ import JellyfinAPI
import Stinsen
import SwiftUI
extension ScheduledTasksView {
extension ServerTasksView {
struct ScheduledTaskButton: View {
struct ServerTaskRow: View {
@CurrentDate
private var currentDate: Date
@ -41,22 +41,6 @@ extension ScheduledTasksView {
}
}
// MARK: - Task Status Section
@ViewBuilder
private var statusView: some View {
switch observer.state {
case .running:
ProgressView(value: (observer.task.currentProgressPercentage ?? 0) / 100)
.progressViewStyle(.gauge(systemImage: "stop.fill"))
.transition(.opacity.combined(with: .scale).animation(.bouncy))
default:
Image(systemName: "play.fill")
.foregroundStyle(.secondary)
.transition(.opacity.combined(with: .scale).animation(.bouncy))
}
}
// MARK: - Task Status View
@ViewBuilder
@ -97,8 +81,16 @@ extension ScheduledTasksView {
Spacer()
statusView
.frame(width: 25, height: 25)
if observer.state == .running {
ProgressView(value: (observer.task.currentProgressPercentage ?? 0) / 100)
.progressViewStyle(.gauge)
.transition(.opacity.combined(with: .scale).animation(.bouncy))
.frame(width: 25, height: 25)
}
Image(systemName: "chevron.right")
.font(.body.weight(.regular))
.foregroundStyle(.secondary)
}
}
.animation(.linear(duration: 0.1), value: observer.state)
@ -122,7 +114,7 @@ extension ScheduledTasksView {
.disabled(observer.task.state == .cancelling)
Button(L10n.edit) {
router.route(to: \.editScheduledTask, observer)
router.route(to: \.editServerTask, observer)
}
} message: {
if let description = observer.task.description {

View File

@ -12,18 +12,13 @@ import SwiftUI
// TODO: refactor after socket implementation
struct ScheduledTasksView: View {
struct ServerTasksView: View {
@EnvironmentObject
private var router: SettingsCoordinator.Router
@State
private var isPresentingRestartConfirmation = false
@State
private var isPresentingShutdownConfirmation = false
@StateObject
private var viewModel = ScheduledTasksViewModel()
private var viewModel = ServerTasksViewModel()
private let timer = Timer.publish(every: 5, on: .main, in: .common)
.autoconnect()
@ -32,20 +27,18 @@ struct ScheduledTasksView: View {
@ViewBuilder
private var serverFunctions: some View {
ServerTaskButton(
DestructiveServerTask(
title: L10n.restartServer,
systemImage: "arrow.clockwise",
warningMessage: L10n.restartWarning,
isPresented: $isPresentingRestartConfirmation
systemName: "arrow.clockwise",
message: L10n.restartWarning
) {
viewModel.send(.restartApplication)
}
ServerTaskButton(
DestructiveServerTask(
title: L10n.shutdownServer,
systemImage: "power",
warningMessage: L10n.shutdownWarning,
isPresented: $isPresentingShutdownConfirmation
systemName: "power",
message: L10n.shutdownWarning
) {
viewModel.send(.shutdownApplication)
}
@ -71,7 +64,7 @@ struct ScheduledTasksView: View {
ForEach(viewModel.tasks.keys, id: \.self) { category in
Section(category) {
ForEach(viewModel.tasks[category] ?? []) { task in
ScheduledTaskButton(observer: task)
ServerTaskRow(observer: task)
}
}
}

View File

@ -734,7 +734,7 @@
"save" = "Save";
/* Time Interval Help Text - Days */
"days" = "days";
"days" = "Days";
/* Section Title for Column Configuration */
"columns" = "Columns";
@ -843,3 +843,183 @@
// Selects all available devices
// Used to select all items in selection mode
"selectAll" = "Select All";
// Logs Description - View
// Access the Jellyfin server logs for troubleshooting and monitoring purposes
// Describes the logs view in settings
"logsDescription" = "Access the Jellyfin server logs for troubleshooting and monitoring purposes.";
/* Indicate a type */
"type" = "Type";
// Day of Week - Section Label
// Specifies the day of the week for the trigger
// Label for the day of week section
"dayOfWeek" = "Day of Week";
// Time - Section Label
// Specifies the time for the trigger
// Label for the time section
"time" = "Time";
// Daily - Description
// Recurring trigger that runs daily
// Describes the daily trigger type
"daily" = "Daily";
// Interval - Description
// Recurring trigger based on time intervals
// Describes the interval trigger type
"interval" = "Interval";
// Weekly - Description
// Recurring trigger that runs weekly
// Describes the weekly trigger type
"weekly" = "Weekly";
// On Application Startup - Description
// Trigger that runs when the application starts
// Describes the startup trigger type
"onApplicationStartup" = "On application startup";
// Task Trigger Time Limit - Section Description
// Sets the maximum runtime (in hours) for this task trigger
// Description for the task trigger time limit section
"taskTriggerTimeLimit" = "Sets the maximum runtime (in hours) for this task trigger.";
// Task Trigger Interval - Section Description
// Sets the duration (in minutes) in between task triggers
// Description for the task trigger interval section
"taskTriggerInterval" = "Sets the duration (in minutes) in between task triggers.";
// Every - Label
// Used to select interval frequency
// Label for selecting interval frequency
"every" = "Every";
// Time Limit with Unit - Label
// Specifies time limit along with the unit
// Time limit label with descriptive unit
"timeLimitWithUnit" = "Time Limit (%@)";
// Time Limit - Section Label
// Specifies the time limit for the task
// Label for the time limit section
"timeLimit" = "Time Limit";
// Hours - Input Field Placeholder
// Placeholder for inputting hours
// Input field placeholder for hours
"hours" = "Hours";
// Minutes - Input Field Placeholder
// Placeholder for inputting minutes
// Input field placeholder for minutes
"minutes" = "Minutes";
// Add Trigger - Title
// Title for adding a new task trigger
// Title for adding a new task trigger
"addTrigger" = "Add trigger";
// Save - Button Label
// Button to save the current task trigger
// Save button label
"save" = "Save";
// Changes Not Saved - Alert Title
// Title for unsaved changes alert
// Title for the unsaved changes alert
"changesNotSaved" = "Changes not saved";
// Discard Changes - Button Label
// Button to discard unsaved changes
// Button label for discarding unsaved changes
"discardChanges" = "Discard Changes";
// Unsaved Changes Message - Alert
// Message for unsaved changes alert
// Alert message for unsaved changes
"unsavedChangesMessage" = "You have unsaved changes. Are you sure you want to discard them?";
// Delete Trigger - Confirmation Dialog Title
// Title for the delete trigger confirmation dialog
// Confirmation dialog title for deleting a trigger
"deleteTrigger" = "Delete Trigger";
// Delete Trigger Confirmation - Message
// Message for deleting a trigger confirmation dialog
// Confirmation dialog message for deleting a trigger
"deleteTriggerConfirmationMessage" = "Are you sure you want to delete this trigger? This action cannot be undone.";
// Item At Item - Label
// Used to describe an item at another item
// Label for something at something else
"itemAtItem" = "%1$@ at %2$@";
// Every Interval - Label
// Describes an interval trigger with recurring time
// Label for interval trigger with recurring time
"everyInterval" = "Every %1$@";
// Time Limit Label with Value - Label
// Describes time limit with a value
// Label for time limit with value
"timeLimitLabelWithValue" = "Time limit: %1$@";
// Add - Button Label
// Button to add a new item
// Button label for adding a new item
"add" = "Add";
// Idle - Task State
// Describes the task state as idle
// Localized text for task state 'Idle'
"idle" = "Idle";
// Status - Section Title
// Title for the status section
// Section title for a status section
"status" = "Status";
// Error Details - Section Title
// Title for the error details section
// Section title for a task error details
"errorDetails" = "Error Details";
// Details - Section Title
// Title for the details section
// Section title for any details section
"details" = "Details";
// Triggers - Section Header
// Header for the scheduled task triggers section
// Section header for scheduled task triggers
"triggers" = "Triggers";
// Executed - Section Title
// Title for the task execution date section
// Section title for a task execution date
"executed" = "Executed";
// No Runtime Limit - Label
// Describes a task with no runtime limit
// No task trigger runtime limit set
"noRuntimeLimit" = "No runtime limit";
// API Key Created - Success Message
// A new Access Token was successfully created for the specified application
// Appears in success alert when a new API key is created
"serverTriggerCreated" = "A new trigger was created for '%1$@'.";
// API Key Deleted - Success Message
// The Access Token was successfully deleted for the specified application
// Appears in success alert when an API key is deleted
"serverTriggerDeleted" = "The selected trigger was deleted from '%1$@'.";
// Save - Button
// Confirms that something completed successfully or without error
// Appears in the views with eventful to indicate a task did not fail
"success" = "Success";
"triggerAlreadyExists" = "Trigger already exists";