[iOS] Admin Dashboard - User Access Schedules (#1358)
* Initial layout. No Add functionality yet. * Cleanup ServerTasks. Get Access Schedules Fixed * duplicate schedule warning, cleanup * localize * cleanup * don't move to Title Case --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
ba5c037ece
commit
d001a96d6c
|
@ -33,10 +33,10 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
// MARK: - Route: Server Tasks
|
// MARK: - Route: Server Tasks
|
||||||
|
|
||||||
@Route(.push)
|
|
||||||
var editServerTask = makeEditServerTask
|
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var tasks = makeTasks
|
var tasks = makeTasks
|
||||||
|
@Route(.push)
|
||||||
|
var editServerTask = makeEditServerTask
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var addServerTaskTrigger = makeAddServerTaskTrigger
|
var addServerTaskTrigger = makeAddServerTaskTrigger
|
||||||
|
|
||||||
|
@ -51,6 +51,11 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
var users = makeUsers
|
var users = makeUsers
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var userDetails = makeUserDetails
|
var userDetails = makeUserDetails
|
||||||
|
@Route(.modal)
|
||||||
|
var addServerUser = makeAddServerUser
|
||||||
|
|
||||||
|
// MARK: - Route: User Policy
|
||||||
|
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var userDeviceAccess = makeUserDeviceAccess
|
var userDeviceAccess = makeUserDeviceAccess
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
|
@ -63,8 +68,10 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
var userParentalRatings = makeUserParentalRatings
|
var userParentalRatings = makeUserParentalRatings
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var resetUserPassword = makeResetUserPassword
|
var resetUserPassword = makeResetUserPassword
|
||||||
|
@Route(.push)
|
||||||
|
var userEditAccessSchedules = makeUserEditAccessSchedules
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var addServerUser = makeAddServerUser
|
var userAddAccessSchedule = makeUserAddAccessSchedule
|
||||||
|
|
||||||
// MARK: - Route: API Keys
|
// MARK: - Route: API Keys
|
||||||
|
|
||||||
|
@ -138,6 +145,8 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Views: User Policy
|
||||||
|
|
||||||
func makeUserDeviceAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
func makeUserDeviceAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
NavigationViewCoordinator {
|
NavigationViewCoordinator {
|
||||||
ServerUserDeviceAccessView(viewModel: viewModel)
|
ServerUserDeviceAccessView(viewModel: viewModel)
|
||||||
|
@ -162,6 +171,17 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeUserEditAccessSchedules(viewModel: ServerUserAdminViewModel) -> some View {
|
||||||
|
EditAccessScheduleView(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUserAddAccessSchedule(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
|
NavigationViewCoordinator {
|
||||||
|
AddAccessScheduleView(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func makeUserParentalRatings(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
func makeUserParentalRatings(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
NavigationViewCoordinator {
|
NavigationViewCoordinator {
|
||||||
ServerUserParentalRatingView(viewModel: viewModel)
|
ServerUserParentalRatingView(viewModel: viewModel)
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
//
|
||||||
|
// 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 DynamicDayOfWeek {
|
||||||
|
|
||||||
|
var displayTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .sunday:
|
||||||
|
DayOfWeek.sunday.displayTitle ?? self.rawValue
|
||||||
|
case .monday:
|
||||||
|
DayOfWeek.monday.displayTitle ?? self.rawValue
|
||||||
|
case .tuesday:
|
||||||
|
DayOfWeek.tuesday.displayTitle ?? self.rawValue
|
||||||
|
case .wednesday:
|
||||||
|
DayOfWeek.wednesday.displayTitle ?? self.rawValue
|
||||||
|
case .thursday:
|
||||||
|
DayOfWeek.thursday.displayTitle ?? self.rawValue
|
||||||
|
case .friday:
|
||||||
|
DayOfWeek.friday.displayTitle ?? self.rawValue
|
||||||
|
case .saturday:
|
||||||
|
DayOfWeek.saturday.displayTitle ?? self.rawValue
|
||||||
|
case .everyday:
|
||||||
|
L10n.everyday
|
||||||
|
case .weekday:
|
||||||
|
L10n.weekday
|
||||||
|
case .weekend:
|
||||||
|
L10n.weekend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
extension Optional where Wrapped: Collection {
|
||||||
|
|
||||||
|
mutating func appendedOrInit(_ element: Wrapped.Element) -> [Wrapped.Element] {
|
||||||
|
if let self {
|
||||||
|
return self + [element]
|
||||||
|
} else {
|
||||||
|
return [element]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,8 @@ extension URL {
|
||||||
|
|
||||||
static let jellyfinDocsUsers: URL = URL(string: "https://jellyfin.org/docs/general/server/users")!
|
static let jellyfinDocsUsers: URL = URL(string: "https://jellyfin.org/docs/general/server/users")!
|
||||||
|
|
||||||
|
static let jellyfinDocsManagingUsers: URL = URL(string: "https://jellyfin.org/docs/general/server/users/adding-managing-users")!
|
||||||
|
|
||||||
func isDirectoryAndReachable() throws -> Bool {
|
func isDirectoryAndReachable() throws -> Bool {
|
||||||
guard try resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true else {
|
guard try resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true else {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -22,10 +22,12 @@ internal enum L10n {
|
||||||
internal static let access = L10n.tr("Localizable", "access", fallback: "Access")
|
internal static let access = L10n.tr("Localizable", "access", fallback: "Access")
|
||||||
/// Accessibility
|
/// Accessibility
|
||||||
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
|
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
|
||||||
/// Access schedule
|
/// The End Time must come after the Start Time.
|
||||||
internal static let accessSchedule = L10n.tr("Localizable", "accessSchedule", fallback: "Access schedule")
|
internal static let accessScheduleInvalidTime = L10n.tr("Localizable", "accessScheduleInvalidTime", fallback: "The End Time must come after the Start Time.")
|
||||||
/// Create an access schedule to limit access to certain hours.
|
/// Access Schedules
|
||||||
internal static let accessScheduleDescription = L10n.tr("Localizable", "accessScheduleDescription", fallback: "Create an access schedule to limit access to certain hours.")
|
internal static let accessSchedules = L10n.tr("Localizable", "accessSchedules", fallback: "Access Schedules")
|
||||||
|
/// Define the allowed hours for usage and restrict access outside those times.
|
||||||
|
internal static let accessSchedulesDescription = L10n.tr("Localizable", "accessSchedulesDescription", fallback: "Define the allowed hours for usage and restrict access outside those times.")
|
||||||
/// Active
|
/// Active
|
||||||
internal static let active = L10n.tr("Localizable", "active", fallback: "Active")
|
internal static let active = L10n.tr("Localizable", "active", fallback: "Active")
|
||||||
/// Active Devices
|
/// Active Devices
|
||||||
|
@ -36,14 +38,16 @@ internal enum L10n {
|
||||||
internal static let actor = L10n.tr("Localizable", "actor", fallback: "Actor")
|
internal static let actor = L10n.tr("Localizable", "actor", fallback: "Actor")
|
||||||
/// Add
|
/// Add
|
||||||
internal static let add = L10n.tr("Localizable", "add", fallback: "Add")
|
internal static let add = L10n.tr("Localizable", "add", fallback: "Add")
|
||||||
|
/// Add Access Schedule
|
||||||
|
internal static let addAccessSchedule = L10n.tr("Localizable", "addAccessSchedule", fallback: "Add Access Schedule")
|
||||||
/// Add API key
|
/// Add API key
|
||||||
internal static let addAPIKey = L10n.tr("Localizable", "addAPIKey", fallback: "Add API key")
|
internal static let addAPIKey = L10n.tr("Localizable", "addAPIKey", fallback: "Add API key")
|
||||||
/// Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.
|
/// Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.
|
||||||
internal static let additionalSecurityAccessDescription = L10n.tr("Localizable", "additionalSecurityAccessDescription", fallback: "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.")
|
internal static let additionalSecurityAccessDescription = L10n.tr("Localizable", "additionalSecurityAccessDescription", fallback: "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.")
|
||||||
/// Add Server
|
/// Add Server
|
||||||
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
|
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
|
||||||
/// Add trigger
|
/// Add Trigger
|
||||||
internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add trigger")
|
internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add Trigger")
|
||||||
/// Add URL
|
/// Add URL
|
||||||
internal static let addURL = L10n.tr("Localizable", "addURL", fallback: "Add URL")
|
internal static let addURL = L10n.tr("Localizable", "addURL", fallback: "Add URL")
|
||||||
/// Add User
|
/// Add User
|
||||||
|
@ -434,14 +438,22 @@ internal enum L10n {
|
||||||
internal static let deleteItemConfirmation = L10n.tr("Localizable", "deleteItemConfirmation", fallback: "Are you sure you want to delete this item?")
|
internal static let deleteItemConfirmation = L10n.tr("Localizable", "deleteItemConfirmation", fallback: "Are you sure you want to delete this item?")
|
||||||
/// Are you sure you want to delete this item? This action cannot be undone.
|
/// Are you sure you want to delete this item? This action cannot be undone.
|
||||||
internal static let deleteItemConfirmationMessage = L10n.tr("Localizable", "deleteItemConfirmationMessage", fallback: "Are you sure you want to delete this item? This action cannot be undone.")
|
internal static let deleteItemConfirmationMessage = L10n.tr("Localizable", "deleteItemConfirmationMessage", fallback: "Are you sure you want to delete this item? This action cannot be undone.")
|
||||||
|
/// Delete Schedule
|
||||||
|
internal static let deleteSchedule = L10n.tr("Localizable", "deleteSchedule", fallback: "Delete Schedule")
|
||||||
|
/// Are you sure you wish to delete this schedule?
|
||||||
|
internal static let deleteScheduleWarning = L10n.tr("Localizable", "deleteScheduleWarning", fallback: "Are you sure you wish to delete this schedule?")
|
||||||
/// Are you sure you want to delete the selected items?
|
/// Are you sure you want to delete the selected items?
|
||||||
internal static let deleteSelectedConfirmation = L10n.tr("Localizable", "deleteSelectedConfirmation", fallback: "Are you sure you want to delete the selected items?")
|
internal static let deleteSelectedConfirmation = L10n.tr("Localizable", "deleteSelectedConfirmation", fallback: "Are you sure you want to delete the selected items?")
|
||||||
/// Delete Selected Devices
|
/// Delete Selected Devices
|
||||||
internal static let deleteSelectedDevices = L10n.tr("Localizable", "deleteSelectedDevices", fallback: "Delete Selected Devices")
|
internal static let deleteSelectedDevices = L10n.tr("Localizable", "deleteSelectedDevices", fallback: "Delete Selected Devices")
|
||||||
|
/// Delete Selected Schedules
|
||||||
|
internal static let deleteSelectedSchedules = L10n.tr("Localizable", "deleteSelectedSchedules", fallback: "Delete Selected Schedules")
|
||||||
/// Delete Selected Users
|
/// Delete Selected Users
|
||||||
internal static let deleteSelectedUsers = L10n.tr("Localizable", "deleteSelectedUsers", fallback: "Delete Selected Users")
|
internal static let deleteSelectedUsers = L10n.tr("Localizable", "deleteSelectedUsers", fallback: "Delete Selected Users")
|
||||||
/// Are you sure you wish to delete all selected devices? All selected sessions will be logged out.
|
/// Are you sure you wish to delete all selected devices? All selected sessions will be logged out.
|
||||||
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.")
|
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.")
|
||||||
|
/// Are you sure you wish to delete all selected schedules?
|
||||||
|
internal static let deleteSelectionSchedulesWarning = L10n.tr("Localizable", "deleteSelectionSchedulesWarning", fallback: "Are you sure you wish to delete all selected schedules?")
|
||||||
/// Are you sure you wish to delete all selected users?
|
/// Are you sure you wish to delete all selected users?
|
||||||
internal static let deleteSelectionUsersWarning = L10n.tr("Localizable", "deleteSelectionUsersWarning", fallback: "Are you sure you wish to delete all selected users?")
|
internal static let deleteSelectionUsersWarning = L10n.tr("Localizable", "deleteSelectionUsersWarning", fallback: "Are you sure you wish to delete all selected users?")
|
||||||
/// Delete Server
|
/// Delete Server
|
||||||
|
@ -546,6 +558,8 @@ internal enum L10n {
|
||||||
internal static let endDate = L10n.tr("Localizable", "endDate", fallback: "End Date")
|
internal static let endDate = L10n.tr("Localizable", "endDate", fallback: "End Date")
|
||||||
/// Ended
|
/// Ended
|
||||||
internal static let ended = L10n.tr("Localizable", "ended", fallback: "Ended")
|
internal static let ended = L10n.tr("Localizable", "ended", fallback: "Ended")
|
||||||
|
/// End Time
|
||||||
|
internal static let endTime = L10n.tr("Localizable", "endTime", fallback: "End Time")
|
||||||
/// Engineer
|
/// Engineer
|
||||||
internal static let engineer = L10n.tr("Localizable", "engineer", fallback: "Engineer")
|
internal static let engineer = L10n.tr("Localizable", "engineer", fallback: "Engineer")
|
||||||
/// Enter custom bitrate in Mbps
|
/// Enter custom bitrate in Mbps
|
||||||
|
@ -582,6 +596,8 @@ internal enum L10n {
|
||||||
internal static let errorDetails = L10n.tr("Localizable", "errorDetails", fallback: "Error Details")
|
internal static let errorDetails = L10n.tr("Localizable", "errorDetails", fallback: "Error Details")
|
||||||
/// Every
|
/// Every
|
||||||
internal static let every = L10n.tr("Localizable", "every", fallback: "Every")
|
internal static let every = L10n.tr("Localizable", "every", fallback: "Every")
|
||||||
|
/// Everyday
|
||||||
|
internal static let everyday = L10n.tr("Localizable", "everyday", fallback: "Everyday")
|
||||||
/// Every %1$@
|
/// Every %1$@
|
||||||
internal static func everyInterval(_ p1: Any) -> String {
|
internal static func everyInterval(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "everyInterval", String(describing: p1), fallback: "Every %1$@")
|
return L10n.tr("Localizable", "everyInterval", String(describing: p1), fallback: "Every %1$@")
|
||||||
|
@ -1188,6 +1204,8 @@ internal enum L10n {
|
||||||
internal static let saveUserWithoutAuthDescription = L10n.tr("Localizable", "saveUserWithoutAuthDescription", fallback: "Save the user to this device without any local authentication.")
|
internal static let saveUserWithoutAuthDescription = L10n.tr("Localizable", "saveUserWithoutAuthDescription", fallback: "Save the user to this device without any local authentication.")
|
||||||
/// Scan All Libraries
|
/// Scan All Libraries
|
||||||
internal static let scanAllLibraries = L10n.tr("Localizable", "scanAllLibraries", fallback: "Scan All Libraries")
|
internal static let scanAllLibraries = L10n.tr("Localizable", "scanAllLibraries", fallback: "Scan All Libraries")
|
||||||
|
/// Schedule already exists
|
||||||
|
internal static let scheduleAlreadyExists = L10n.tr("Localizable", "scheduleAlreadyExists", fallback: "Schedule already exists")
|
||||||
/// Scheduled Tasks
|
/// Scheduled Tasks
|
||||||
internal static let scheduledTasks = L10n.tr("Localizable", "scheduledTasks", fallback: "Scheduled Tasks")
|
internal static let scheduledTasks = L10n.tr("Localizable", "scheduledTasks", fallback: "Scheduled Tasks")
|
||||||
/// Scrub Current Time
|
/// Scrub Current Time
|
||||||
|
@ -1330,6 +1348,8 @@ internal enum L10n {
|
||||||
internal static let specialFeatures = L10n.tr("Localizable", "specialFeatures", fallback: "Special Features")
|
internal static let specialFeatures = L10n.tr("Localizable", "specialFeatures", fallback: "Special Features")
|
||||||
/// Sports
|
/// Sports
|
||||||
internal static let sports = L10n.tr("Localizable", "sports", fallback: "Sports")
|
internal static let sports = L10n.tr("Localizable", "sports", fallback: "Sports")
|
||||||
|
/// Start Time
|
||||||
|
internal static let startTime = L10n.tr("Localizable", "startTime", fallback: "Start Time")
|
||||||
/// Status
|
/// Status
|
||||||
internal static let status = L10n.tr("Localizable", "status", fallback: "Status")
|
internal static let status = L10n.tr("Localizable", "status", fallback: "Status")
|
||||||
/// Stop
|
/// Stop
|
||||||
|
@ -1546,6 +1566,10 @@ internal enum L10n {
|
||||||
internal static let videoResolutionNotSupported = L10n.tr("Localizable", "videoResolutionNotSupported", fallback: "The video resolution is not supported")
|
internal static let videoResolutionNotSupported = L10n.tr("Localizable", "videoResolutionNotSupported", fallback: "The video resolution is not supported")
|
||||||
/// Video transcoding
|
/// Video transcoding
|
||||||
internal static let videoTranscoding = L10n.tr("Localizable", "videoTranscoding", fallback: "Video transcoding")
|
internal static let videoTranscoding = L10n.tr("Localizable", "videoTranscoding", fallback: "Video transcoding")
|
||||||
|
/// Weekday
|
||||||
|
internal static let weekday = L10n.tr("Localizable", "weekday", fallback: "Weekday")
|
||||||
|
/// Weekend
|
||||||
|
internal static let weekend = L10n.tr("Localizable", "weekend", fallback: "Weekend")
|
||||||
/// Weekly
|
/// Weekly
|
||||||
internal static let weekly = L10n.tr("Localizable", "weekly", fallback: "Weekly")
|
internal static let weekly = L10n.tr("Localizable", "weekly", fallback: "Weekly")
|
||||||
/// Who's watching?
|
/// Who's watching?
|
||||||
|
|
|
@ -200,6 +200,11 @@
|
||||||
4EC6C16B2C92999800FC904B /* TranscodeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */; };
|
4EC6C16B2C92999800FC904B /* TranscodeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */; };
|
||||||
4ECDAA9E2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; };
|
4ECDAA9E2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; };
|
||||||
4ECDAA9F2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; };
|
4ECDAA9F2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; };
|
||||||
|
4ECF5D882D0A3D0200F066B1 /* AddAccessScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */; };
|
||||||
|
4ECF5D8A2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */; };
|
||||||
|
4ECF5D8B2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */; };
|
||||||
|
4ED25CA12D07E3590010333C /* EditAccessScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */; };
|
||||||
|
4ED25CA42D07E4990010333C /* EditAccessScheduleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */; };
|
||||||
4EE07CBB2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; };
|
4EE07CBB2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; };
|
||||||
4EE07CBC2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; };
|
4EE07CBC2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; };
|
||||||
4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */; };
|
4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */; };
|
||||||
|
@ -916,6 +921,8 @@
|
||||||
E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */; };
|
E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */; };
|
||||||
E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */; };
|
E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */; };
|
||||||
E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */; };
|
E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */; };
|
||||||
|
E1A5056A2D0B733F007EE305 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A505692D0B733F007EE305 /* Optional.swift */; };
|
||||||
|
E1A5056B2D0B733F007EE305 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A505692D0B733F007EE305 /* Optional.swift */; };
|
||||||
E1A7B1652B9A9F7800152546 /* PreferencesView in Frameworks */ = {isa = PBXBuildFile; productRef = E1A7B1642B9A9F7800152546 /* PreferencesView */; };
|
E1A7B1652B9A9F7800152546 /* PreferencesView in Frameworks */ = {isa = PBXBuildFile; productRef = E1A7B1642B9A9F7800152546 /* PreferencesView */; };
|
||||||
E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; };
|
E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; };
|
||||||
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */; };
|
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */; };
|
||||||
|
@ -1311,6 +1318,10 @@
|
||||||
4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksViewModel.swift; sourceTree = "<group>"; };
|
4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksViewModel.swift; sourceTree = "<group>"; };
|
||||||
4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = "<group>"; };
|
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>"; };
|
4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = "<group>"; };
|
||||||
|
4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessScheduleView.swift; sourceTree = "<group>"; };
|
||||||
|
4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDayOfWeek.swift; sourceTree = "<group>"; };
|
||||||
|
4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleView.swift; sourceTree = "<group>"; };
|
||||||
|
4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleRow.swift; sourceTree = "<group>"; };
|
||||||
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = "<group>"; };
|
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = "<group>"; };
|
||||||
4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
|
4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
|
||||||
4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = "<group>"; };
|
4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1782,6 +1793,7 @@
|
||||||
E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemSelector.swift; sourceTree = "<group>"; };
|
E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemSelector.swift; sourceTree = "<group>"; };
|
||||||
E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = "<group>"; };
|
E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = "<group>"; };
|
||||||
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; };
|
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; };
|
||||||
|
E1A505692D0B733F007EE305 /* Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Optional.swift; sourceTree = "<group>"; };
|
||||||
E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
|
E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
|
||||||
E1A8FDEB2C0574A800D0A51C /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
|
E1A8FDEB2C0574A800D0A51C /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
|
||||||
E1AA331C2782541500F6439C /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; };
|
E1AA331C2782541500F6439C /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2302,19 +2314,18 @@
|
||||||
4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */,
|
4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */,
|
||||||
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */,
|
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */,
|
||||||
4EB7C8D32CCED318000CC011 /* AddServerUserView */,
|
4EB7C8D32CCED318000CC011 /* AddServerUserView */,
|
||||||
4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */,
|
|
||||||
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */,
|
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */,
|
||||||
4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */,
|
4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */,
|
||||||
E1DE64902CC6F06C00E423B6 /* Components */,
|
E1DE64902CC6F06C00E423B6 /* Components */,
|
||||||
4E10C80F2CC030B20012CC9F /* DeviceDetailsView */,
|
4E10C80F2CC030B20012CC9F /* DeviceDetailsView */,
|
||||||
4EED87492CBF824B002354D2 /* DevicesView */,
|
4EED87492CBF824B002354D2 /* DevicesView */,
|
||||||
4E90F7622CC72B1F00417C31 /* EditServerTaskView */,
|
|
||||||
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
||||||
4E182C9A2C94991800FBEFD5 /* ServerTasksView */,
|
4ECF5D8C2D0A780F00F066B1 /* ServerTasks */,
|
||||||
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
||||||
4E2470072D078DD7009139D8 /* ServerUserParentalRatingView */,
|
4E2470072D078DD7009139D8 /* ServerUserParentalRatingView */,
|
||||||
4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */,
|
4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */,
|
||||||
4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */,
|
4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */,
|
||||||
|
4ED25C9F2D07E20C0010333C /* ServerUserAccessSchedule */,
|
||||||
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */,
|
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */,
|
||||||
4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */,
|
4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */,
|
||||||
4EC2B1992CC96E5E00D866BE /* ServerUsersView */,
|
4EC2B1992CC96E5E00D866BE /* ServerUsersView */,
|
||||||
|
@ -2633,6 +2644,50 @@
|
||||||
path = ServerUserDetailsView;
|
path = ServerUserDetailsView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4ECF5D822D0A3D0200F066B1 /* AddAccessScheduleView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */,
|
||||||
|
);
|
||||||
|
path = AddAccessScheduleView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4ECF5D8C2D0A780F00F066B1 /* ServerTasks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */,
|
||||||
|
4E90F7622CC72B1F00417C31 /* EditServerTaskView */,
|
||||||
|
4E182C9A2C94991800FBEFD5 /* ServerTasksView */,
|
||||||
|
);
|
||||||
|
path = ServerTasks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4ED25C9F2D07E20C0010333C /* ServerUserAccessSchedule */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4ECF5D822D0A3D0200F066B1 /* AddAccessScheduleView */,
|
||||||
|
4ED25CA52D07E64F0010333C /* EditAccessScheduleView */,
|
||||||
|
);
|
||||||
|
path = ServerUserAccessSchedule;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4ED25CA32D07E4990010333C /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4ED25CA52D07E64F0010333C /* EditAccessScheduleView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4ED25CA32D07E4990010333C /* Components */,
|
||||||
|
4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */,
|
||||||
|
);
|
||||||
|
path = EditAccessScheduleView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4EED87472CBF824B002354D2 /* Components */ = {
|
4EED87472CBF824B002354D2 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -3162,6 +3217,7 @@
|
||||||
E1AD105226D96D5F003E4A08 /* JellyfinAPI */,
|
E1AD105226D96D5F003E4A08 /* JellyfinAPI */,
|
||||||
E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */,
|
E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */,
|
||||||
E150C0B82BFD44E900944FFA /* Nuke */,
|
E150C0B82BFD44E900944FFA /* Nuke */,
|
||||||
|
E1A505692D0B733F007EE305 /* Optional.swift */,
|
||||||
E1B4E4362CA7795200DC49DE /* OrderedDictionary.swift */,
|
E1B4E4362CA7795200DC49DE /* OrderedDictionary.swift */,
|
||||||
E1B490432967E26300D3EDCE /* PersistentLogHandler.swift */,
|
E1B490432967E26300D3EDCE /* PersistentLogHandler.swift */,
|
||||||
E1B5861129E32EEF00E45D6E /* Sequence.swift */,
|
E1B5861129E32EEF00E45D6E /* Sequence.swift */,
|
||||||
|
@ -4235,8 +4291,8 @@
|
||||||
4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */,
|
4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */,
|
||||||
4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */,
|
4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */,
|
||||||
4E12F9152CBE9615006C217E /* DeviceType.swift */,
|
4E12F9152CBE9615006C217E /* DeviceType.swift */,
|
||||||
4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */,
|
|
||||||
E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */,
|
E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */,
|
||||||
|
4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */,
|
||||||
E1722DB029491C3900CC0239 /* ImageBlurHashes.swift */,
|
E1722DB029491C3900CC0239 /* ImageBlurHashes.swift */,
|
||||||
E1D842902933F87500D1041A /* ItemFields.swift */,
|
E1D842902933F87500D1041A /* ItemFields.swift */,
|
||||||
E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */,
|
E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */,
|
||||||
|
@ -4253,6 +4309,7 @@
|
||||||
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */,
|
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */,
|
||||||
4E2182E42CAF67EF0094806B /* PlayMethod.swift */,
|
4E2182E42CAF67EF0094806B /* PlayMethod.swift */,
|
||||||
4E35CE652CBED8B300DBD886 /* ServerTicks.swift */,
|
4E35CE652CBED8B300DBD886 /* ServerTicks.swift */,
|
||||||
|
4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */,
|
||||||
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */,
|
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */,
|
||||||
E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */,
|
E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */,
|
||||||
E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */,
|
E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */,
|
||||||
|
@ -5119,6 +5176,7 @@
|
||||||
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */,
|
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */,
|
||||||
E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
||||||
62E632E1267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */,
|
62E632E1267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */,
|
||||||
|
4ECF5D8B2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */,
|
||||||
62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||||
5398514526B64DA100101B49 /* SettingsView.swift in Sources */,
|
5398514526B64DA100101B49 /* SettingsView.swift in Sources */,
|
||||||
E193D54B271941D300900D82 /* SelectServerView.swift in Sources */,
|
E193D54B271941D300900D82 /* SelectServerView.swift in Sources */,
|
||||||
|
@ -5333,6 +5391,7 @@
|
||||||
E18E021C2887492B0022598C /* BlurView.swift in Sources */,
|
E18E021C2887492B0022598C /* BlurView.swift in Sources */,
|
||||||
E187F7682B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */,
|
E187F7682B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */,
|
||||||
E1E6C44729AECD5D0064123F /* PlayPreviousItemActionButton.swift in Sources */,
|
E1E6C44729AECD5D0064123F /* PlayPreviousItemActionButton.swift in Sources */,
|
||||||
|
E1A5056B2D0B733F007EE305 /* Optional.swift in Sources */,
|
||||||
E1E6C44E29AEE9DC0064123F /* SmallMenuOverlay.swift in Sources */,
|
E1E6C44E29AEE9DC0064123F /* SmallMenuOverlay.swift in Sources */,
|
||||||
E1CB75832C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
|
E1CB75832C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
|
||||||
E10B1ECB2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */,
|
E10B1ECB2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */,
|
||||||
|
@ -5402,6 +5461,7 @@
|
||||||
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */,
|
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */,
|
||||||
4E2470082D078DD7009139D8 /* ServerUserParentalRatingView.swift in Sources */,
|
4E2470082D078DD7009139D8 /* ServerUserParentalRatingView.swift in Sources */,
|
||||||
E1A1528828FD229500600579 /* ChevronButton.swift in Sources */,
|
E1A1528828FD229500600579 /* ChevronButton.swift in Sources */,
|
||||||
|
4ECF5D882D0A3D0200F066B1 /* AddAccessScheduleView.swift in Sources */,
|
||||||
E1CB75732C80E71800217C76 /* DirectPlayProfile.swift in Sources */,
|
E1CB75732C80E71800217C76 /* DirectPlayProfile.swift in Sources */,
|
||||||
E1B490472967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
E1B490472967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
||||||
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
|
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
|
||||||
|
@ -5533,6 +5593,7 @@
|
||||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||||
E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */,
|
E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */,
|
||||||
|
E1A5056A2D0B733F007EE305 /* Optional.swift in Sources */,
|
||||||
E102314D2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */,
|
E102314D2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */,
|
||||||
E12CC1BB28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */,
|
E12CC1BB28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */,
|
||||||
E1BE1CEE2BDB68CD008176A9 /* UserProfileRow.swift in Sources */,
|
E1BE1CEE2BDB68CD008176A9 /* UserProfileRow.swift in Sources */,
|
||||||
|
@ -5552,6 +5613,7 @@
|
||||||
4EBE064F2C7ECE8D004A6C03 /* InlineEnumToggle.swift in Sources */,
|
4EBE064F2C7ECE8D004A6C03 /* InlineEnumToggle.swift in Sources */,
|
||||||
E14EDEC82B8FB65F000F00A4 /* ItemFilterType.swift in Sources */,
|
E14EDEC82B8FB65F000F00A4 /* ItemFilterType.swift in Sources */,
|
||||||
E1EBCB42278BD174009FE6E9 /* TruncatedText.swift in Sources */,
|
E1EBCB42278BD174009FE6E9 /* TruncatedText.swift in Sources */,
|
||||||
|
4ED25CA12D07E3590010333C /* EditAccessScheduleView.swift in Sources */,
|
||||||
62133890265F83A900A81A2A /* MediaView.swift in Sources */,
|
62133890265F83A900A81A2A /* MediaView.swift in Sources */,
|
||||||
E13332942953BAA100EE76AB /* DownloadTaskContentView.swift in Sources */,
|
E13332942953BAA100EE76AB /* DownloadTaskContentView.swift in Sources */,
|
||||||
4E14DC032CD43DD2001B621B /* AdminDashboardCoordinator.swift in Sources */,
|
4E14DC032CD43DD2001B621B /* AdminDashboardCoordinator.swift in Sources */,
|
||||||
|
@ -5618,6 +5680,7 @@
|
||||||
E11E0E8C2BF7E76F007676DD /* DataCache.swift in Sources */,
|
E11E0E8C2BF7E76F007676DD /* DataCache.swift in Sources */,
|
||||||
E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
|
E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
|
||||||
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
||||||
|
4ED25CA42D07E4990010333C /* EditAccessScheduleRow.swift in Sources */,
|
||||||
4E49DEE02CE55F7F00352DCD /* PhotoPicker.swift in Sources */,
|
4E49DEE02CE55F7F00352DCD /* PhotoPicker.swift in Sources */,
|
||||||
4E49DEE12CE55F7F00352DCD /* SquareImageCropView.swift in Sources */,
|
4E49DEE12CE55F7F00352DCD /* SquareImageCropView.swift in Sources */,
|
||||||
4E90F7642CC72B1F00417C31 /* LastRunSection.swift in Sources */,
|
4E90F7642CC72B1F00417C31 /* LastRunSection.swift in Sources */,
|
||||||
|
@ -5686,6 +5749,7 @@
|
||||||
E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */,
|
E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */,
|
||||||
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
||||||
E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
|
E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
|
||||||
|
4ECF5D8A2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */,
|
||||||
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
|
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
|
||||||
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */,
|
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */,
|
||||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||||
|
|
|
@ -46,11 +46,6 @@ struct AddServerUserView: View {
|
||||||
@State
|
@State
|
||||||
private var confirmPassword: String = ""
|
private var confirmPassword: String = ""
|
||||||
|
|
||||||
// MARK: - Dialog State
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var isPresentingSuccess: Bool = false
|
|
||||||
|
|
||||||
// MARK: - Error State
|
// MARK: - Error State
|
||||||
|
|
||||||
@State
|
@State
|
||||||
|
|
|
@ -25,7 +25,7 @@ struct ServerLogsView: View {
|
||||||
private var contentView: some View {
|
private var contentView: some View {
|
||||||
List {
|
List {
|
||||||
ListTitleSection(
|
ListTitleSection(
|
||||||
L10n.logs,
|
L10n.serverLogs,
|
||||||
description: L10n.logsDescription
|
description: L10n.logsDescription
|
||||||
) {
|
) {
|
||||||
UIApplication.shared.open(URL(string: "https://jellyfin.org/docs/general/administration/troubleshooting")!)
|
UIApplication.shared.open(URL(string: "https://jellyfin.org/docs/general/administration/troubleshooting")!)
|
||||||
|
|
|
@ -12,23 +12,24 @@ import SwiftUI
|
||||||
|
|
||||||
struct EditServerTaskView: View {
|
struct EditServerTaskView: View {
|
||||||
|
|
||||||
|
// MARK: - Observed & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: AdminDashboardCoordinator.Router
|
private var router: AdminDashboardCoordinator.Router
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var observer: ServerTaskObserver
|
var observer: ServerTaskObserver
|
||||||
|
|
||||||
// MARK: - State Variables
|
// MARK: - Trigger Variables
|
||||||
|
|
||||||
@State
|
|
||||||
private var isPresentingDeleteConfirmation = false
|
|
||||||
@State
|
|
||||||
private var isPresentingEventAlert = false
|
|
||||||
@State
|
|
||||||
private var error: JellyfinAPIError?
|
|
||||||
@State
|
@State
|
||||||
private var selectedTrigger: TaskTriggerInfo?
|
private var selectedTrigger: TaskTriggerInfo?
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -78,17 +79,8 @@ struct EditServerTaskView: View {
|
||||||
switch event {
|
switch event {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingEventAlert = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
.errorMessage($error)
|
||||||
L10n.error,
|
|
||||||
isPresented: $isPresentingEventAlert,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
//
|
||||||
|
// 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 AddAccessScheduleView: View {
|
||||||
|
|
||||||
|
// MARK: - Observed & Environment Objects
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
private var viewModel: ServerUserAdminViewModel
|
||||||
|
|
||||||
|
// MARK: - Access Schedule Variables
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var tempPolicy: UserPolicy
|
||||||
|
@State
|
||||||
|
private var selectedDay: DynamicDayOfWeek = .everyday
|
||||||
|
@State
|
||||||
|
private var startTime: Date = Calendar.current.startOfDay(for: Date())
|
||||||
|
@State
|
||||||
|
private var endTime: Date = Calendar.current.startOfDay(for: Date()).addingTimeInterval(+3600)
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init(viewModel: ServerUserAdminViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.tempPolicy = viewModel.user.policy!
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isValidRange: Bool {
|
||||||
|
startTime < endTime
|
||||||
|
}
|
||||||
|
|
||||||
|
private var newSchedule: AccessSchedule? {
|
||||||
|
guard isValidRange else { return nil }
|
||||||
|
|
||||||
|
let calendar = Calendar.current
|
||||||
|
let startComponents = calendar.dateComponents([.hour, .minute], from: startTime)
|
||||||
|
let endComponents = calendar.dateComponents([.hour, .minute], from: endTime)
|
||||||
|
|
||||||
|
guard let startHour = startComponents.hour,
|
||||||
|
let startMinute = startComponents.minute,
|
||||||
|
let endHour = endComponents.hour,
|
||||||
|
let endMinute = endComponents.minute
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessSchedule Hours are formatted as 23.5 == 11:30pm or 8.25 == 8:15am
|
||||||
|
let startDouble = Double(startHour) + Double(startMinute) / 60.0
|
||||||
|
let endDouble = Double(endHour) + Double(endMinute) / 60.0
|
||||||
|
|
||||||
|
// AccessSchedule should have valid Start & End Hours
|
||||||
|
let newSchedule = AccessSchedule(
|
||||||
|
dayOfWeek: selectedDay,
|
||||||
|
endHour: endDouble,
|
||||||
|
startHour: startDouble,
|
||||||
|
userID: viewModel.user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return newSchedule
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isDuplicateSchedule: Bool {
|
||||||
|
guard let newSchedule, let existingSchedules = viewModel.user.policy?.accessSchedules else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingSchedules.contains { other in
|
||||||
|
other.dayOfWeek == selectedDay &&
|
||||||
|
other.startHour == newSchedule.startHour &&
|
||||||
|
other.endHour == newSchedule.endHour
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
contentView
|
||||||
|
.navigationTitle(L10n.addAccessSchedule)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarCloseButton {
|
||||||
|
router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
.topBarTrailing {
|
||||||
|
if viewModel.backgroundStates.contains(.refreshing) {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
if viewModel.backgroundStates.contains(.updating) {
|
||||||
|
Button(L10n.cancel) {
|
||||||
|
viewModel.send(.cancel)
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill(.red))
|
||||||
|
} else {
|
||||||
|
Button(L10n.save) {
|
||||||
|
saveSchedule()
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.disabled(!isValidRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(viewModel.events) { event in
|
||||||
|
switch event {
|
||||||
|
case let .error(eventError):
|
||||||
|
UIDevice.feedback(.error)
|
||||||
|
error = eventError
|
||||||
|
case .updated:
|
||||||
|
UIDevice.feedback(.success)
|
||||||
|
router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.errorMessage($error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Content View
|
||||||
|
|
||||||
|
private var contentView: some View {
|
||||||
|
Form {
|
||||||
|
Section(L10n.dayOfWeek) {
|
||||||
|
Picker(L10n.dayOfWeek, selection: $selectedDay) {
|
||||||
|
ForEach(DynamicDayOfWeek.allCases, id: \.self) { day in
|
||||||
|
|
||||||
|
if day == .everyday {
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(day.displayTitle).tag(day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(L10n.startTime) {
|
||||||
|
DatePicker(L10n.startTime, selection: $startTime, displayedComponents: .hourAndMinute)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
DatePicker(L10n.endTime, selection: $endTime, displayedComponents: .hourAndMinute)
|
||||||
|
} header: {
|
||||||
|
Text(L10n.endTime)
|
||||||
|
} footer: {
|
||||||
|
if !isValidRange {
|
||||||
|
Label(L10n.accessScheduleInvalidTime, systemImage: "exclamationmark.circle.fill")
|
||||||
|
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDuplicateSchedule {
|
||||||
|
Label(L10n.scheduleAlreadyExists, systemImage: "exclamationmark.circle.fill")
|
||||||
|
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Save Schedule
|
||||||
|
|
||||||
|
private func saveSchedule() {
|
||||||
|
|
||||||
|
guard isValidRange, let newSchedule else {
|
||||||
|
error = JellyfinAPIError(L10n.accessScheduleInvalidTime)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !isDuplicateSchedule else {
|
||||||
|
error = JellyfinAPIError(L10n.scheduleAlreadyExists)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tempPolicy.accessSchedules = tempPolicy.accessSchedules
|
||||||
|
.appendedOrInit(newSchedule)
|
||||||
|
|
||||||
|
viewModel.send(.updatePolicy(tempPolicy))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditAccessScheduleView {
|
||||||
|
|
||||||
|
struct EditAccessScheduleRow: View {
|
||||||
|
|
||||||
|
// MARK: - Environment Variables
|
||||||
|
|
||||||
|
@Environment(\.isEditing)
|
||||||
|
private var isEditing
|
||||||
|
@Environment(\.isSelected)
|
||||||
|
private var isSelected
|
||||||
|
|
||||||
|
// MARK: - Schedule Variable
|
||||||
|
|
||||||
|
let schedule: AccessSchedule
|
||||||
|
|
||||||
|
// MARK: - Schedule Actions
|
||||||
|
|
||||||
|
let onSelect: () -> Void
|
||||||
|
let onDelete: () -> Void
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: onSelect) {
|
||||||
|
rowContent
|
||||||
|
}
|
||||||
|
.foregroundStyle(.primary, .secondary)
|
||||||
|
.swipeActions {
|
||||||
|
Button(L10n.delete, systemImage: "trash", action: onDelete)
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Row Content
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var rowContent: some View {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if let dayOfWeek = schedule.dayOfWeek {
|
||||||
|
Text(dayOfWeek.rawValue)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
|
||||||
|
Group {
|
||||||
|
if let startHour = schedule.startHour {
|
||||||
|
TextPairView(
|
||||||
|
leading: L10n.startTime,
|
||||||
|
trailing: doubleToTimeString(startHour)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let endHour = schedule.endHour {
|
||||||
|
TextPairView(
|
||||||
|
leading: L10n.endTime,
|
||||||
|
trailing: doubleToTimeString(endHour)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.foregroundStyle(
|
||||||
|
isEditing ? (isSelected ? .primary : .secondary) : .primary,
|
||||||
|
.secondary
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
ListRowCheckbox()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Convert Double to Date
|
||||||
|
|
||||||
|
private func doubleToTimeString(_ double: Double) -> String {
|
||||||
|
let startHours = Int(double)
|
||||||
|
let startMinutes = Int(double.truncatingRemainder(dividingBy: 1) * 60)
|
||||||
|
|
||||||
|
var dateComponents = DateComponents()
|
||||||
|
dateComponents.hour = startHours
|
||||||
|
dateComponents.minute = startMinutes
|
||||||
|
|
||||||
|
let calendar = Calendar.current
|
||||||
|
|
||||||
|
guard let date = calendar.date(from: dateComponents) else {
|
||||||
|
return .emptyTime
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.timeStyle = .short
|
||||||
|
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EditAccessScheduleView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
|
@Default(.accentColor)
|
||||||
|
private var accentColor
|
||||||
|
|
||||||
|
// MARK: - Observed & Environment Objects
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: AdminDashboardCoordinator.Router
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
private var viewModel: ServerUserAdminViewModel
|
||||||
|
|
||||||
|
// MARK: - Policy Variable
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var selectedSchedules: Set<AccessSchedule> = []
|
||||||
|
|
||||||
|
// MARK: - Dialog States
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isPresentingDeleteSelectionConfirmation = false
|
||||||
|
@State
|
||||||
|
private var isPresentingDeleteConfirmation = false
|
||||||
|
|
||||||
|
// MARK: - Editing State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isEditing: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init(viewModel: ServerUserAdminViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
contentView
|
||||||
|
.navigationTitle(L10n.accessSchedules)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarBackButtonHidden(isEditing)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
|
if isEditing {
|
||||||
|
navigationBarSelectView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
if isEditing {
|
||||||
|
Button(L10n.cancel) {
|
||||||
|
isEditing.toggle()
|
||||||
|
selectedSchedules.removeAll()
|
||||||
|
UIDevice.impact(.light)
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.foregroundStyle(accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .bottomBar) {
|
||||||
|
if isEditing {
|
||||||
|
Button(L10n.delete) {
|
||||||
|
isPresentingDeleteSelectionConfirmation = true
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill(.red))
|
||||||
|
.disabled(selectedSchedules.isEmpty)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarMenuButton(
|
||||||
|
isLoading: viewModel.backgroundStates.contains(.refreshing),
|
||||||
|
isHidden: isEditing || viewModel.user.policy?.accessSchedules == []
|
||||||
|
) {
|
||||||
|
Button(L10n.add, systemImage: "plus") {
|
||||||
|
router.route(to: \.userAddAccessSchedule, viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(L10n.edit, systemImage: "checkmark.circle") {
|
||||||
|
isEditing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(viewModel.events) { event in
|
||||||
|
switch event {
|
||||||
|
case let .error(eventError):
|
||||||
|
UIDevice.feedback(.error)
|
||||||
|
error = eventError
|
||||||
|
case .updated:
|
||||||
|
UIDevice.feedback(.success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
L10n.deleteSelectedSchedules,
|
||||||
|
isPresented: $isPresentingDeleteSelectionConfirmation,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
deleteSelectedSchedulesConfirmationActions
|
||||||
|
} message: {
|
||||||
|
Text(L10n.deleteSelectionSchedulesWarning)
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
L10n.deleteSchedule,
|
||||||
|
isPresented: $isPresentingDeleteConfirmation,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
deleteScheduleConfirmationActions
|
||||||
|
} message: {
|
||||||
|
Text(L10n.deleteScheduleWarning)
|
||||||
|
}
|
||||||
|
.errorMessage($error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Content View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var contentView: some View {
|
||||||
|
List {
|
||||||
|
ListTitleSection(
|
||||||
|
L10n.accessSchedules.localizedCapitalized,
|
||||||
|
description: L10n.accessSchedulesDescription
|
||||||
|
) {
|
||||||
|
UIApplication.shared.open(.jellyfinDocsManagingUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.user.policy?.accessSchedules == [] {
|
||||||
|
Button(L10n.add) {
|
||||||
|
router.route(to: \.userAddAccessSchedule, viewModel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ForEach(viewModel.user.policy?.accessSchedules ?? [], id: \.self) { schedule in
|
||||||
|
EditAccessScheduleRow(schedule: schedule) {
|
||||||
|
if isEditing {
|
||||||
|
selectedSchedules.toggle(value: schedule)
|
||||||
|
}
|
||||||
|
} onDelete: {
|
||||||
|
selectedSchedules = [schedule]
|
||||||
|
isPresentingDeleteConfirmation = true
|
||||||
|
}
|
||||||
|
.environment(\.isEditing, isEditing)
|
||||||
|
.environment(\.isSelected, selectedSchedules.contains(schedule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Navigation Bar Select/Remove All Content
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var navigationBarSelectView: some View {
|
||||||
|
|
||||||
|
let isAllSelected: Bool = selectedSchedules.count == viewModel.user.policy?.accessSchedules?.count
|
||||||
|
|
||||||
|
Button(isAllSelected ? L10n.removeAll : L10n.selectAll) {
|
||||||
|
if isAllSelected {
|
||||||
|
selectedSchedules = []
|
||||||
|
} else {
|
||||||
|
selectedSchedules = Set(viewModel.user.policy?.accessSchedules ?? [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.disabled(!isEditing)
|
||||||
|
.foregroundStyle(accentColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Delete Selected Schedules Confirmation Actions
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var deleteSelectedSchedulesConfirmationActions: some View {
|
||||||
|
Button(L10n.cancel, role: .cancel) {}
|
||||||
|
|
||||||
|
Button(L10n.confirm, role: .destructive) {
|
||||||
|
|
||||||
|
var tempPolicy: UserPolicy = viewModel.user.policy!
|
||||||
|
|
||||||
|
if selectedSchedules.isNotEmpty {
|
||||||
|
tempPolicy.accessSchedules = tempPolicy.accessSchedules?.filter { !selectedSchedules.contains($0)
|
||||||
|
}
|
||||||
|
viewModel.send(.updatePolicy(tempPolicy))
|
||||||
|
isEditing = false
|
||||||
|
selectedSchedules.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Delete Schedule Confirmation Actions
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var deleteScheduleConfirmationActions: some View {
|
||||||
|
Button(L10n.cancel, role: .cancel) {}
|
||||||
|
|
||||||
|
Button(L10n.delete, role: .destructive) {
|
||||||
|
|
||||||
|
var tempPolicy: UserPolicy = viewModel.user.policy!
|
||||||
|
|
||||||
|
if let scheduleToDelete = selectedSchedules.first,
|
||||||
|
selectedSchedules.count == 1
|
||||||
|
{
|
||||||
|
tempPolicy.accessSchedules = tempPolicy.accessSchedules?.filter {
|
||||||
|
$0 != scheduleToDelete
|
||||||
|
}
|
||||||
|
viewModel.send(.updatePolicy(tempPolicy))
|
||||||
|
isEditing = false
|
||||||
|
selectedSchedules.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,6 +58,9 @@ struct ServerUserMediaAccessView: View {
|
||||||
.buttonStyle(.toolbarPill)
|
.buttonStyle(.toolbarPill)
|
||||||
.disabled(viewModel.user.policy == tempPolicy)
|
.disabled(viewModel.user.policy == tempPolicy)
|
||||||
}
|
}
|
||||||
|
.onFirstAppear {
|
||||||
|
viewModel.send(.loadLibraries())
|
||||||
|
}
|
||||||
.onReceive(viewModel.events) { event in
|
.onReceive(viewModel.events) { event in
|
||||||
switch event {
|
switch event {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
|
|
|
@ -68,13 +68,12 @@ struct ServerUserDetailsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(L10n.parentalControls) {
|
Section(L10n.parentalControls) {
|
||||||
// TODO: Access Schedules - accessSchedules
|
ChevronButton(L10n.accessSchedules)
|
||||||
/* ChevronButton("Access schedule")
|
.onSelect {
|
||||||
.onSelect {
|
router.route(to: \.userEditAccessSchedules, viewModel)
|
||||||
router.route(to: \.userAccessSchedules, viewModel)
|
}
|
||||||
}
|
// TODO: Allow items SDK 10.10 - allowedTags
|
||||||
// TODO: Allow items SDK 10.10 - allowedTags
|
/* ChevronButton("Allow items")
|
||||||
ChevronButton("Allow items")
|
|
||||||
.onSelect {
|
.onSelect {
|
||||||
router.route(to: \.userAllowedTags, viewModel)
|
router.route(to: \.userAllowedTags, viewModel)
|
||||||
}
|
}
|
||||||
|
@ -82,7 +81,7 @@ struct ServerUserDetailsView: View {
|
||||||
ChevronButton("Block items")
|
ChevronButton("Block items")
|
||||||
.onSelect {
|
.onSelect {
|
||||||
router.route(to: \.userBlockedTags, viewModel)
|
router.route(to: \.userBlockedTags, viewModel)
|
||||||
}*/
|
} */
|
||||||
ChevronButton(L10n.ratings)
|
ChevronButton(L10n.ratings)
|
||||||
.onSelect {
|
.onSelect {
|
||||||
router.route(to: \.userParentalRatings, viewModel)
|
router.route(to: \.userParentalRatings, viewModel)
|
||||||
|
|
|
@ -17,7 +17,7 @@ struct ServerUserLiveTVAccessView: View {
|
||||||
@CurrentDate
|
@CurrentDate
|
||||||
private var currentDate: Date
|
private var currentDate: Date
|
||||||
|
|
||||||
// MARK: - State & Environment Objects
|
// MARK: - Observed & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: BasicNavigationViewCoordinator.Router
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
|
@ -2049,6 +2049,26 @@
|
||||||
// Represents a translator
|
// Represents a translator
|
||||||
"translator" = "Translator";
|
"translator" = "Translator";
|
||||||
|
|
||||||
|
// Start Time - Label
|
||||||
|
// Label for selecting or displaying the start time
|
||||||
|
"startTime" = "Start Time";
|
||||||
|
|
||||||
|
// End Time - Label
|
||||||
|
// Label for selecting or displaying the end time
|
||||||
|
"endTime" = "End Time";
|
||||||
|
|
||||||
|
// Access Schedules - Label
|
||||||
|
// Label for configuring or viewing the access schedule
|
||||||
|
"accessSchedules" = "Access Schedules";
|
||||||
|
|
||||||
|
// Add Access Schedule - Label
|
||||||
|
// Label for adding a single Access Schedule
|
||||||
|
"addAccessSchedule" = "Add Access Schedule";
|
||||||
|
|
||||||
|
// Access Schedule Description - Label
|
||||||
|
// Description for viewing or listing multiple schedules
|
||||||
|
"accessSchedulesDescription" = "Define the allowed hours for usage and restrict access outside those times.";
|
||||||
|
|
||||||
// Parental controls - Section Title
|
// Parental controls - Section Title
|
||||||
// Parental controls section & view titles
|
// Parental controls section & view titles
|
||||||
"parentalControls" = "Parental controls";
|
"parentalControls" = "Parental controls";
|
||||||
|
@ -2085,14 +2105,6 @@
|
||||||
// Parental ratings description for blocked tags
|
// Parental ratings description for blocked tags
|
||||||
"blockedTagsDescription" = "Hide media with at least one of the specified tags.";
|
"blockedTagsDescription" = "Hide media with at least one of the specified tags.";
|
||||||
|
|
||||||
// Access Schedule - View Title
|
|
||||||
// Parental ratings section for blocked titles
|
|
||||||
"accessSchedule" = "Access schedule";
|
|
||||||
|
|
||||||
// Access Schedule - Footer
|
|
||||||
// Parental ratings section for allowed titles
|
|
||||||
"accessScheduleDescription" = "Create an access schedule to limit access to certain hours.";
|
|
||||||
|
|
||||||
// Trailers - Section Title
|
// Trailers - Section Title
|
||||||
// Title for content classified as trailers
|
// Title for content classified as trailers
|
||||||
"trailers" = "Trailers";
|
"trailers" = "Trailers";
|
||||||
|
@ -2252,3 +2264,39 @@
|
||||||
// Ages Group - Group Name
|
// Ages Group - Group Name
|
||||||
// Label for content suitable for a specific age group
|
// Label for content suitable for a specific age group
|
||||||
"agesGroup" = "Age %@";
|
"agesGroup" = "Age %@";
|
||||||
|
|
||||||
|
// End Time must come after Start Time - Access Schedule Error
|
||||||
|
// Error produced when trying to create
|
||||||
|
"accessScheduleInvalidTime" = "The End Time must come after the Start Time.";
|
||||||
|
|
||||||
|
// Everyday - Label
|
||||||
|
// DynamicDayOfWeek label for Everyday
|
||||||
|
"everyday" = "Everyday";
|
||||||
|
|
||||||
|
// Weekday - Label
|
||||||
|
// DynamicDayOfWeek label for Weekdays
|
||||||
|
"weekday" = "Weekday";
|
||||||
|
|
||||||
|
// Weekend - Label
|
||||||
|
// DynamicDayOfWeek label for the Weekend
|
||||||
|
"weekend" = "Weekend";
|
||||||
|
|
||||||
|
// Schedule Already Exists -
|
||||||
|
// Message to indicate that an Access Schedule already exists
|
||||||
|
"scheduleAlreadyExists" = "Schedule already exists";
|
||||||
|
|
||||||
|
// Delete Selected Schedules Warning - Warning Message
|
||||||
|
// Warning message displayed when deleting all schedules
|
||||||
|
"deleteSelectionSchedulesWarning" = "Are you sure you wish to delete all selected schedules?";
|
||||||
|
|
||||||
|
// Delete Schedule Warning - Warning Message
|
||||||
|
// Warning message displayed when deleting a single schedules
|
||||||
|
"deleteScheduleWarning" = "Are you sure you wish to delete this schedule?";
|
||||||
|
|
||||||
|
// Delete Schedule - Action
|
||||||
|
// Message for deleting a single Access Schedule
|
||||||
|
"deleteSchedule" = "Delete Schedule";
|
||||||
|
|
||||||
|
// Delete Selected Schedules - Button
|
||||||
|
// Button label for deleting all selected Access Schedules
|
||||||
|
"deleteSelectedSchedules" = "Delete Selected Schedules";
|
||||||
|
|
Loading…
Reference in New Issue