mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-23 05:59:51 +00:00
[iOS] Admin Dashboard - Add/Delete Task Triggers (#1276)
* 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:
parent
a04f97e1ba
commit
c46ee13dbc
@ -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
|
||||
|
25
Shared/Extensions/JellyfinAPI/DayOfWeek.swift
Normal file
25
Shared/Extensions/JellyfinAPI/DayOfWeek.swift
Normal 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
|
||||
}
|
||||
}
|
87
Shared/Extensions/JellyfinAPI/ServerTicks.swift
Normal file
87
Shared/Extensions/JellyfinAPI/ServerTicks.swift
Normal 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()
|
||||
}
|
||||
}
|
24
Shared/Extensions/JellyfinAPI/TaskState.swift
Normal file
24
Shared/Extensions/JellyfinAPI/TaskState.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
45
Shared/Extensions/JellyfinAPI/TaskTriggerType.swift
Normal file
45
Shared/Extensions/JellyfinAPI/TaskTriggerType.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 */,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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!)
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user