[iOS] Admin Dashboard - User Access Tags (#1377)
* Edit View. Still need to make an Add View * Finished with EditPage. Need labels tho * Deletion deletes TOO many records. Also, need to search existing tags * Fin * Fix merge issues * Check for exisitng Access Tags before allowing saving * 2025 Disclaimer / Build Fixes * update * Update EditServerUserAccessTagsView.swift --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
adec8de122
commit
a13f604be0
|
@ -72,6 +72,10 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
var userEditAccessSchedules = makeUserEditAccessSchedules
|
var userEditAccessSchedules = makeUserEditAccessSchedules
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var userAddAccessSchedule = makeUserAddAccessSchedule
|
var userAddAccessSchedule = makeUserAddAccessSchedule
|
||||||
|
@Route(.push)
|
||||||
|
var userEditAccessTags = makeUserEditAccessTags
|
||||||
|
@Route(.modal)
|
||||||
|
var userAddAccessTag = makeUserAddAccessTag
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var userPhotoPicker = makeUserPhotoPicker
|
var userPhotoPicker = makeUserPhotoPicker
|
||||||
|
|
||||||
|
@ -182,6 +186,17 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
EditAccessScheduleView(viewModel: viewModel)
|
EditAccessScheduleView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeUserEditAccessTags(viewModel: ServerUserAdminViewModel) -> some View {
|
||||||
|
EditServerUserAccessTagsView(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUserAddAccessTag(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
|
NavigationViewCoordinator {
|
||||||
|
AddServerUserAccessTagsView(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func makeUserAddAccessSchedule(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
func makeUserAddAccessSchedule(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
NavigationViewCoordinator {
|
NavigationViewCoordinator {
|
||||||
AddAccessScheduleView(viewModel: viewModel)
|
AddAccessScheduleView(viewModel: viewModel)
|
||||||
|
|
|
@ -26,6 +26,16 @@ internal enum L10n {
|
||||||
internal static let accessSchedules = L10n.tr("Localizable", "accessSchedules", fallback: "Access Schedules")
|
internal static let accessSchedules = L10n.tr("Localizable", "accessSchedules", fallback: "Access Schedules")
|
||||||
/// Define the allowed hours for usage and restrict access outside those times.
|
/// 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.")
|
internal static let accessSchedulesDescription = L10n.tr("Localizable", "accessSchedulesDescription", fallback: "Define the allowed hours for usage and restrict access outside those times.")
|
||||||
|
/// User will have access to no media unless it contains at least one allowed tag.
|
||||||
|
internal static let accessTagAllowDescription = L10n.tr("Localizable", "accessTagAllowDescription", fallback: "User will have access to no media unless it contains at least one allowed tag.")
|
||||||
|
/// Access tag already exists
|
||||||
|
internal static let accessTagAlreadyExists = L10n.tr("Localizable", "accessTagAlreadyExists", fallback: "Access tag already exists")
|
||||||
|
/// User will have access to all media except when it contains any blocked tag.
|
||||||
|
internal static let accessTagBlockDescription = L10n.tr("Localizable", "accessTagBlockDescription", fallback: "User will have access to all media except when it contains any blocked tag.")
|
||||||
|
/// Access Tags
|
||||||
|
internal static let accessTags = L10n.tr("Localizable", "accessTags", fallback: "Access Tags")
|
||||||
|
/// Use tags to grant or restrict this user's access to media.
|
||||||
|
internal static let accessTagsDescription = L10n.tr("Localizable", "accessTagsDescription", fallback: "Use tags to grant or restrict this user's access to media.")
|
||||||
/// Active
|
/// Active
|
||||||
internal static let active = L10n.tr("Localizable", "active", fallback: "Active")
|
internal static let active = L10n.tr("Localizable", "active", fallback: "Active")
|
||||||
/// Activity
|
/// Activity
|
||||||
|
@ -34,8 +44,10 @@ 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
|
/// Add access schedule
|
||||||
internal static let addAccessSchedule = L10n.tr("Localizable", "addAccessSchedule", fallback: "Add Access Schedule")
|
internal static let addAccessSchedule = L10n.tr("Localizable", "addAccessSchedule", fallback: "Add access schedule")
|
||||||
|
/// Add access tag
|
||||||
|
internal static let addAccessTag = L10n.tr("Localizable", "addAccessTag", fallback: "Add access tag")
|
||||||
/// 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.
|
||||||
|
@ -74,6 +86,8 @@ internal enum L10n {
|
||||||
internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media")
|
internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media")
|
||||||
/// Allow collection management
|
/// Allow collection management
|
||||||
internal static let allowCollectionManagement = L10n.tr("Localizable", "allowCollectionManagement", fallback: "Allow collection management")
|
internal static let allowCollectionManagement = L10n.tr("Localizable", "allowCollectionManagement", fallback: "Allow collection management")
|
||||||
|
/// Allowed
|
||||||
|
internal static let allowed = L10n.tr("Localizable", "allowed", fallback: "Allowed")
|
||||||
/// Allow media item deletion
|
/// Allow media item deletion
|
||||||
internal static let allowItemDeletion = L10n.tr("Localizable", "allowItemDeletion", fallback: "Allow media item deletion")
|
internal static let allowItemDeletion = L10n.tr("Localizable", "allowItemDeletion", fallback: "Allow media item deletion")
|
||||||
/// Allow media item editing
|
/// Allow media item editing
|
||||||
|
@ -198,6 +212,8 @@ internal enum L10n {
|
||||||
internal static let bitrateTestDisclaimer = L10n.tr("Localizable", "bitrateTestDisclaimer", fallback: "Longer tests are more accurate but may result in a delayed playback.")
|
internal static let bitrateTestDisclaimer = L10n.tr("Localizable", "bitrateTestDisclaimer", fallback: "Longer tests are more accurate but may result in a delayed playback.")
|
||||||
/// bps
|
/// bps
|
||||||
internal static let bitsPerSecond = L10n.tr("Localizable", "bitsPerSecond", fallback: "bps")
|
internal static let bitsPerSecond = L10n.tr("Localizable", "bitsPerSecond", fallback: "bps")
|
||||||
|
/// Blocked
|
||||||
|
internal static let blocked = L10n.tr("Localizable", "blocked", fallback: "Blocked")
|
||||||
/// Block unrated items
|
/// Block unrated items
|
||||||
internal static let blockUnratedItems = L10n.tr("Localizable", "blockUnratedItems", fallback: "Block unrated items")
|
internal static let blockUnratedItems = L10n.tr("Localizable", "blockUnratedItems", fallback: "Block unrated items")
|
||||||
/// Block items from this user with no or unrecognized rating information.
|
/// Block items from this user with no or unrecognized rating information.
|
||||||
|
@ -1202,6 +1218,8 @@ internal enum L10n {
|
||||||
internal static let syncPlay = L10n.tr("Localizable", "syncPlay", fallback: "SyncPlay")
|
internal static let syncPlay = L10n.tr("Localizable", "syncPlay", fallback: "SyncPlay")
|
||||||
/// System
|
/// System
|
||||||
internal static let system = L10n.tr("Localizable", "system", fallback: "System")
|
internal static let system = L10n.tr("Localizable", "system", fallback: "System")
|
||||||
|
/// Tag
|
||||||
|
internal static let tag = L10n.tr("Localizable", "tag", fallback: "Tag")
|
||||||
/// Tagline
|
/// Tagline
|
||||||
internal static let tagline = L10n.tr("Localizable", "tagline", fallback: "Tagline")
|
internal static let tagline = L10n.tr("Localizable", "tagline", fallback: "Tagline")
|
||||||
/// Taglines
|
/// Taglines
|
||||||
|
|
|
@ -279,6 +279,8 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
|
||||||
|
|
||||||
// MARK: - Reorder Elements (To Be Overridden)
|
// MARK: - Reorder Elements (To Be Overridden)
|
||||||
|
|
||||||
|
// TODO: should instead move to an index-based self insertion
|
||||||
|
// instead of replacement
|
||||||
func reorderComponents(_ tags: [Element]) async throws {
|
func reorderComponents(_ tags: [Element]) async throws {
|
||||||
fatalError("This method should be overridden in subclasses")
|
fatalError("This method should be overridden in subclasses")
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,6 +232,11 @@
|
||||||
4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; };
|
4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; };
|
||||||
4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */; };
|
4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */; };
|
||||||
4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */; };
|
4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */; };
|
||||||
|
4EFAC12C2D1E255900E40880 /* EditServerUserAccessTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */; };
|
||||||
|
4EFAC1302D1E2EB900E40880 /* EditAccessTagRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC12E2D1E2EB900E40880 /* EditAccessTagRow.swift */; };
|
||||||
|
4EFAC1332D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC1322D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift */; };
|
||||||
|
4EFAC1362D1FB1A100E40880 /* AccessTagSearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC1352D1FB1A100E40880 /* AccessTagSearchResultsSection.swift */; };
|
||||||
|
4EFAC1382D1FB26600E40880 /* TagInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC1372D1FB26600E40880 /* TagInput.swift */; };
|
||||||
4EFD172E2CE4182200A4BAC5 /* LearnMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */; };
|
4EFD172E2CE4182200A4BAC5 /* LearnMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */; };
|
||||||
4EFE0C7D2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; };
|
4EFE0C7D2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; };
|
||||||
4EFE0C7E2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; };
|
4EFE0C7E2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; };
|
||||||
|
@ -1361,6 +1366,11 @@
|
||||||
4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
|
4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
|
||||||
4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserAccessView.swift; sourceTree = "<group>"; };
|
4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserAccessView.swift; sourceTree = "<group>"; };
|
||||||
4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMenu.swift; sourceTree = "<group>"; };
|
4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMenu.swift; sourceTree = "<group>"; };
|
||||||
|
4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServerUserAccessTagsView.swift; sourceTree = "<group>"; };
|
||||||
|
4EFAC12E2D1E2EB900E40880 /* EditAccessTagRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessTagRow.swift; sourceTree = "<group>"; };
|
||||||
|
4EFAC1322D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddServerUserAccessTagsView.swift; sourceTree = "<group>"; };
|
||||||
|
4EFAC1352D1FB1A100E40880 /* AccessTagSearchResultsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessTagSearchResultsSection.swift; sourceTree = "<group>"; };
|
||||||
|
4EFAC1372D1FB26600E40880 /* TagInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagInput.swift; sourceTree = "<group>"; };
|
||||||
4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreButton.swift; sourceTree = "<group>"; };
|
4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreButton.swift; sourceTree = "<group>"; };
|
||||||
4EFE0C7C2D0156A500D4834D /* PersonKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonKind.swift; sourceTree = "<group>"; };
|
4EFE0C7C2D0156A500D4834D /* PersonKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonKind.swift; sourceTree = "<group>"; };
|
||||||
4EFE0C7F2D02054300D4834D /* ItemArrayElements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemArrayElements.swift; sourceTree = "<group>"; };
|
4EFE0C7F2D02054300D4834D /* ItemArrayElements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemArrayElements.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2373,6 +2383,7 @@
|
||||||
4EED87492CBF824B002354D2 /* DevicesView */,
|
4EED87492CBF824B002354D2 /* DevicesView */,
|
||||||
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
||||||
4ECF5D8C2D0A780F00F066B1 /* ServerTasks */,
|
4ECF5D8C2D0A780F00F066B1 /* ServerTasks */,
|
||||||
|
4EFAC12A2D1E253300E40880 /* ServerUserAccessTags */,
|
||||||
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
||||||
4E2470072D078DD7009139D8 /* ServerUserParentalRatingView */,
|
4E2470072D078DD7009139D8 /* ServerUserParentalRatingView */,
|
||||||
4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */,
|
4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */,
|
||||||
|
@ -2861,6 +2872,50 @@
|
||||||
path = ServerUserAccessView;
|
path = ServerUserAccessView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4EFAC12A2D1E253300E40880 /* ServerUserAccessTags */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4EFAC1312D1E373B00E40880 /* AddServerUserAccessTagsView */,
|
||||||
|
4EFAC12D2D1E2C4700E40880 /* EditServerUserAccessTagsView */,
|
||||||
|
);
|
||||||
|
path = ServerUserAccessTags;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4EFAC12D2D1E2C4700E40880 /* EditServerUserAccessTagsView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4EFAC12F2D1E2EB900E40880 /* Components */,
|
||||||
|
4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */,
|
||||||
|
);
|
||||||
|
path = EditServerUserAccessTagsView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4EFAC12F2D1E2EB900E40880 /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4EFAC12E2D1E2EB900E40880 /* EditAccessTagRow.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4EFAC1312D1E373B00E40880 /* AddServerUserAccessTagsView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4EFAC1342D1FB19700E40880 /* Components */,
|
||||||
|
4EFAC1322D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift */,
|
||||||
|
);
|
||||||
|
path = AddServerUserAccessTagsView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
4EFAC1342D1FB19700E40880 /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4EFAC1352D1FB1A100E40880 /* AccessTagSearchResultsSection.swift */,
|
||||||
|
4EFAC1372D1FB26600E40880 /* TagInput.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
5310694F2684E7EE00CFFDBA /* VideoPlayer */ = {
|
5310694F2684E7EE00CFFDBA /* VideoPlayer */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -5632,6 +5687,7 @@
|
||||||
E1B33ECF28EB6EA90073B0FD /* OverlayMenu.swift in Sources */,
|
E1B33ECF28EB6EA90073B0FD /* OverlayMenu.swift in Sources */,
|
||||||
E146A9D82BE6E9830034DA1E /* StoredValue.swift in Sources */,
|
E146A9D82BE6E9830034DA1E /* StoredValue.swift in Sources */,
|
||||||
6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */,
|
6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */,
|
||||||
|
4EFAC1362D1FB1A100E40880 /* AccessTagSearchResultsSection.swift in Sources */,
|
||||||
E17AC96D2954E9CA003D2BC2 /* DownloadListView.swift in Sources */,
|
E17AC96D2954E9CA003D2BC2 /* DownloadListView.swift in Sources */,
|
||||||
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */,
|
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */,
|
||||||
4E2470082D078DD7009139D8 /* ServerUserParentalRatingView.swift in Sources */,
|
4E2470082D078DD7009139D8 /* ServerUserParentalRatingView.swift in Sources */,
|
||||||
|
@ -5766,12 +5822,14 @@
|
||||||
4E2AC4C82C6C493C00DD600D /* SubtitleFormat.swift in Sources */,
|
4E2AC4C82C6C493C00DD600D /* SubtitleFormat.swift in Sources */,
|
||||||
E19D41B02BF2B7540082B8B2 /* URLSessionConfiguration.swift in Sources */,
|
E19D41B02BF2B7540082B8B2 /* URLSessionConfiguration.swift in Sources */,
|
||||||
4E661A2E2CEFE77700025C99 /* MetadataField.swift in Sources */,
|
4E661A2E2CEFE77700025C99 /* MetadataField.swift in Sources */,
|
||||||
|
4EFAC1332D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift in Sources */,
|
||||||
E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */,
|
E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */,
|
||||||
4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */,
|
4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */,
|
||||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||||
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 */,
|
E1A5056A2D0B733F007EE305 /* Optional.swift in Sources */,
|
||||||
|
4EFAC1382D1FB26600E40880 /* TagInput.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 */,
|
||||||
|
@ -5974,6 +6032,7 @@
|
||||||
E1CAF6622BA363840087D991 /* UIHostingController.swift in Sources */,
|
E1CAF6622BA363840087D991 /* UIHostingController.swift in Sources */,
|
||||||
E11895AC289383EE0042947B /* NavigationBarOffsetModifier.swift in Sources */,
|
E11895AC289383EE0042947B /* NavigationBarOffsetModifier.swift in Sources */,
|
||||||
E1CD13EF28EF364100CB46CA /* DetectOrientationModifier.swift in Sources */,
|
E1CD13EF28EF364100CB46CA /* DetectOrientationModifier.swift in Sources */,
|
||||||
|
4EFAC12C2D1E255900E40880 /* EditServerUserAccessTagsView.swift in Sources */,
|
||||||
E157563029355B7900976E1F /* UpdateView.swift in Sources */,
|
E157563029355B7900976E1F /* UpdateView.swift in Sources */,
|
||||||
E1D8424F2932F7C400D1041A /* OverviewView.swift in Sources */,
|
E1D8424F2932F7C400D1041A /* OverviewView.swift in Sources */,
|
||||||
E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */,
|
E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */,
|
||||||
|
@ -6090,6 +6149,7 @@
|
||||||
E113133828BEADBA00930F75 /* LibraryParent.swift in Sources */,
|
E113133828BEADBA00930F75 /* LibraryParent.swift in Sources */,
|
||||||
E17DC74D2BE7601E00B42379 /* SettingsBarButton.swift in Sources */,
|
E17DC74D2BE7601E00B42379 /* SettingsBarButton.swift in Sources */,
|
||||||
E190704F2C8592B40004600E /* PlaybackCompatibility+Video.swift in Sources */,
|
E190704F2C8592B40004600E /* PlaybackCompatibility+Video.swift in Sources */,
|
||||||
|
4EFAC1302D1E2EB900E40880 /* EditAccessTagRow.swift in Sources */,
|
||||||
E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */,
|
E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */,
|
||||||
E102312C2BCF8A08009D71FC /* iOSLiveTVCoordinator.swift in Sources */,
|
E102312C2BCF8A08009D71FC /* iOSLiveTVCoordinator.swift in Sources */,
|
||||||
E1ED7FDC2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */,
|
E1ED7FDC2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */,
|
||||||
|
|
|
@ -92,7 +92,7 @@ struct AddAccessScheduleView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
contentView
|
contentView
|
||||||
.navigationTitle(L10n.addAccessSchedule)
|
.navigationTitle(L10n.addAccessSchedule.localizedCapitalized)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationBarCloseButton {
|
.navigationBarCloseButton {
|
||||||
router.dismissCoordinator()
|
router.dismissCoordinator()
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AddServerUserAccessTagsView: View {
|
||||||
|
|
||||||
|
// MARK: - Observed & Environment Objects
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
private var viewModel: ServerUserAdminViewModel
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var tagViewModel: TagEditorViewModel
|
||||||
|
|
||||||
|
// MARK: - Access Tag Variables
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var tempPolicy: UserPolicy
|
||||||
|
@State
|
||||||
|
private var tempTag: String = ""
|
||||||
|
@State
|
||||||
|
private var access: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
|
// MARK: - Name is Valid
|
||||||
|
|
||||||
|
private var isValid: Bool {
|
||||||
|
tempTag.isNotEmpty && !tagIsDuplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tag is Already Blocked/Allowed
|
||||||
|
|
||||||
|
private var tagIsDuplicate: Bool {
|
||||||
|
viewModel.user.policy!.blockedTags!.contains(tempTag) // &&
|
||||||
|
//! viewModel.user.policy!.allowedTags!.contains(tempTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tag Already Exists on Jellyfin
|
||||||
|
|
||||||
|
private var tagAlreadyExists: Bool {
|
||||||
|
tagViewModel.trie.contains(key: tempTag.localizedLowercase)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init(viewModel: ServerUserAdminViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.tempPolicy = viewModel.user.policy!
|
||||||
|
self._tagViewModel = StateObject(wrappedValue: TagEditorViewModel(item: .init()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
contentView
|
||||||
|
.navigationTitle(L10n.addAccessTag.localizedCapitalized)
|
||||||
|
.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) {
|
||||||
|
if access {
|
||||||
|
// TODO: Enable on 10.10
|
||||||
|
/* tempPolicy.allowedTags = tempPolicy.allowedTags
|
||||||
|
.appendedOrInit(tempTag) */
|
||||||
|
} else {
|
||||||
|
tempPolicy.blockedTags = tempPolicy.blockedTags
|
||||||
|
.appendedOrInit(tempTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.send(.updatePolicy(tempPolicy))
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.disabled(!isValid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFirstAppear {
|
||||||
|
tagViewModel.send(.load)
|
||||||
|
}
|
||||||
|
.onChange(of: tempTag) { _ in
|
||||||
|
if !tagViewModel.backgroundStates.contains(.loading) {
|
||||||
|
tagViewModel.send(.search(tempTag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(viewModel.events) { event in
|
||||||
|
switch event {
|
||||||
|
case let .error(eventError):
|
||||||
|
UIDevice.feedback(.error)
|
||||||
|
error = eventError
|
||||||
|
case .updated:
|
||||||
|
UIDevice.feedback(.success)
|
||||||
|
router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(tagViewModel.events) { event in
|
||||||
|
switch event {
|
||||||
|
case .updated:
|
||||||
|
break
|
||||||
|
case .loaded:
|
||||||
|
tagViewModel.send(.search(tempTag))
|
||||||
|
case let .error(eventError):
|
||||||
|
UIDevice.feedback(.error)
|
||||||
|
error = eventError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.errorMessage($error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Content View
|
||||||
|
|
||||||
|
private var contentView: some View {
|
||||||
|
Form {
|
||||||
|
TagInput(
|
||||||
|
access: $access,
|
||||||
|
tag: $tempTag,
|
||||||
|
tagIsDuplicate: tagIsDuplicate,
|
||||||
|
tagAlreadyExists: tagAlreadyExists
|
||||||
|
)
|
||||||
|
|
||||||
|
SearchResultsSection(
|
||||||
|
tag: $tempTag,
|
||||||
|
tags: tagViewModel.matches,
|
||||||
|
isSearching: tagViewModel.backgroundStates.contains(.searching)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension AddServerUserAccessTagsView {
|
||||||
|
|
||||||
|
struct SearchResultsSection: View {
|
||||||
|
|
||||||
|
// MARK: - Element Variables
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var tag: String
|
||||||
|
|
||||||
|
// MARK: - Element Search Variables
|
||||||
|
|
||||||
|
let tags: [String]
|
||||||
|
let isSearching: Bool
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if tag.isNotEmpty {
|
||||||
|
Section {
|
||||||
|
if tags.isNotEmpty {
|
||||||
|
resultsView
|
||||||
|
} else if !isSearching {
|
||||||
|
noResultsView
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
Text(L10n.existingItems)
|
||||||
|
|
||||||
|
if isSearching {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Text("-")
|
||||||
|
|
||||||
|
Text(tags.count, format: .number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.linear(duration: 0.2), value: tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - No Results View
|
||||||
|
|
||||||
|
private var noResultsView: some View {
|
||||||
|
Text(L10n.none)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Results View
|
||||||
|
|
||||||
|
private var resultsView: some View {
|
||||||
|
ForEach(tags, id: \.self) { result in
|
||||||
|
Button(result) {
|
||||||
|
tag = result
|
||||||
|
}
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
.disabled(tag == result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension AddServerUserAccessTagsView {
|
||||||
|
|
||||||
|
struct TagInput: View {
|
||||||
|
|
||||||
|
// MARK: - Element Variables
|
||||||
|
|
||||||
|
@FocusState
|
||||||
|
private var isTagFocused: Bool
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var access: Bool
|
||||||
|
@Binding
|
||||||
|
var tag: String
|
||||||
|
|
||||||
|
let tagIsDuplicate: Bool
|
||||||
|
let tagAlreadyExists: Bool
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
// TODO: Enable on 10.10
|
||||||
|
// Section {
|
||||||
|
// Picker(L10n.access, selection: $access) {
|
||||||
|
// Text(L10n.allowed).tag(true)
|
||||||
|
// Text(L10n.blocked).tag(false)
|
||||||
|
// }
|
||||||
|
// .disabled(true)
|
||||||
|
// } header: {
|
||||||
|
// Text(L10n.access)
|
||||||
|
// } footer: {
|
||||||
|
// LearnMoreButton(L10n.accessTags) {
|
||||||
|
// TextPair(
|
||||||
|
// title: L10n.allowed,
|
||||||
|
// subtitle: L10n.accessTagAllowDescription
|
||||||
|
// )
|
||||||
|
// TextPair(
|
||||||
|
// title: L10n.blocked,
|
||||||
|
// subtitle: L10n.accessTagBlockDescription
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
Section {
|
||||||
|
TextField(L10n.name, text: $tag)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
.focused($isTagFocused)
|
||||||
|
} footer: {
|
||||||
|
if tag.isEmpty {
|
||||||
|
Label(
|
||||||
|
L10n.required,
|
||||||
|
systemImage: "exclamationmark.circle.fill"
|
||||||
|
)
|
||||||
|
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||||
|
} else if tagIsDuplicate {
|
||||||
|
Label(
|
||||||
|
L10n.accessTagAlreadyExists,
|
||||||
|
systemImage: "exclamationmark.circle.fill"
|
||||||
|
)
|
||||||
|
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||||
|
} else {
|
||||||
|
if tagAlreadyExists {
|
||||||
|
Label(
|
||||||
|
L10n.existsOnServer,
|
||||||
|
systemImage: "checkmark.circle.fill"
|
||||||
|
)
|
||||||
|
.labelStyle(.sectionFooterWithImage(imageStyle: .green))
|
||||||
|
} else {
|
||||||
|
Label(
|
||||||
|
L10n.willBeCreatedOnServer,
|
||||||
|
systemImage: "checkmark.seal.fill"
|
||||||
|
)
|
||||||
|
.labelStyle(.sectionFooterWithImage(imageStyle: .blue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFirstAppear {
|
||||||
|
isTagFocused = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EditServerUserAccessTagsView {
|
||||||
|
|
||||||
|
struct EditAccessTagRow: View {
|
||||||
|
|
||||||
|
// MARK: - Metadata Variables
|
||||||
|
|
||||||
|
let tag: String
|
||||||
|
|
||||||
|
// MARK: - Row Actions
|
||||||
|
|
||||||
|
let onSelect: () -> Void
|
||||||
|
let onDelete: () -> Void
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: onSelect) {
|
||||||
|
HStack {
|
||||||
|
Text(tag)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
ListRowCheckbox()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
.swipeActions {
|
||||||
|
Button(
|
||||||
|
L10n.delete,
|
||||||
|
systemImage: "trash",
|
||||||
|
action: onDelete
|
||||||
|
)
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EditServerUserAccessTagsView: View {
|
||||||
|
|
||||||
|
private struct TagWithAccess: Hashable {
|
||||||
|
let tag: String
|
||||||
|
let access: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Observed, State, & Environment Objects
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: AdminDashboardCoordinator.Router
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var viewModel: ServerUserAdminViewModel
|
||||||
|
|
||||||
|
// MARK: - Dialog States
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isPresentingDeleteConfirmation = false
|
||||||
|
|
||||||
|
// MARK: - Editing States
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var selectedTags: Set<TagWithAccess> = []
|
||||||
|
@State
|
||||||
|
private var isEditing: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
|
private var blockedTags: [TagWithAccess] {
|
||||||
|
viewModel.user.policy?.blockedTags?
|
||||||
|
.sorted()
|
||||||
|
.map { TagWithAccess(tag: $0, access: false) } ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
// private var allowedTags: [TagWithAccess] {
|
||||||
|
// viewModel.user.policy?.allowedTags?
|
||||||
|
// .sorted()
|
||||||
|
// .map { TagWithAccess(tag: $0, access: true) } ?? []
|
||||||
|
// }
|
||||||
|
|
||||||
|
// MARK: - Initializera
|
||||||
|
|
||||||
|
init(viewModel: ServerUserAdminViewModel) {
|
||||||
|
self._viewModel = StateObject(wrappedValue: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
switch viewModel.state {
|
||||||
|
case .initial, .content:
|
||||||
|
contentView
|
||||||
|
case let .error(error):
|
||||||
|
errorView(with: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(L10n.accessTags)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarBackButtonHidden(isEditing)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
|
if isEditing {
|
||||||
|
navigationBarSelectView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
if isEditing {
|
||||||
|
Button(L10n.cancel) {
|
||||||
|
isEditing = false
|
||||||
|
UIDevice.impact(.light)
|
||||||
|
selectedTags.removeAll()
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .bottomBar) {
|
||||||
|
if isEditing {
|
||||||
|
Button(L10n.delete) {
|
||||||
|
isPresentingDeleteConfirmation = true
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill(.red))
|
||||||
|
.disabled(selectedTags.isEmpty)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarMenuButton(
|
||||||
|
isLoading: viewModel.backgroundStates.contains(.refreshing),
|
||||||
|
isHidden: isEditing || (
|
||||||
|
viewModel.user.policy?.blockedTags?.isEmpty == true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Button(L10n.add, systemImage: "plus") {
|
||||||
|
router.route(to: \.userAddAccessTag, viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.user.policy?.blockedTags?.isNotEmpty == true {
|
||||||
|
Button(L10n.edit, systemImage: "checkmark.circle") {
|
||||||
|
isEditing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(viewModel.events) { event in
|
||||||
|
switch event {
|
||||||
|
case let .error(eventError):
|
||||||
|
error = eventError
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
L10n.delete,
|
||||||
|
isPresented: $isPresentingDeleteConfirmation,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
deleteSelectedConfirmationActions
|
||||||
|
} message: {
|
||||||
|
Text(L10n.deleteSelectedConfirmation)
|
||||||
|
}
|
||||||
|
.errorMessage($error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ErrorView
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func errorView(with error: some Error) -> some View {
|
||||||
|
ErrorView(error: error)
|
||||||
|
.onRetry {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func makeRow(tag: TagWithAccess) -> some View {
|
||||||
|
EditAccessTagRow(tag: tag.tag) {
|
||||||
|
if isEditing {
|
||||||
|
selectedTags.toggle(value: tag)
|
||||||
|
}
|
||||||
|
} onDelete: {
|
||||||
|
selectedTags = [tag]
|
||||||
|
isPresentingDeleteConfirmation = true
|
||||||
|
}
|
||||||
|
.environment(\.isEditing, isEditing)
|
||||||
|
.environment(\.isSelected, selectedTags.contains(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Content View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var contentView: some View {
|
||||||
|
List {
|
||||||
|
ListTitleSection(
|
||||||
|
L10n.accessTags,
|
||||||
|
description: L10n.accessTagsDescription
|
||||||
|
) {
|
||||||
|
UIApplication.shared.open(.jellyfinDocsManagingUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if blockedTags.isEmpty {
|
||||||
|
Button(L10n.add) {
|
||||||
|
router.route(to: \.userAddAccessTag, viewModel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// TODO: with allowed, use `DisclosureGroup` instead
|
||||||
|
Section(L10n.blocked) {
|
||||||
|
ForEach(
|
||||||
|
blockedTags,
|
||||||
|
id: \.self,
|
||||||
|
content: makeRow
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: allowed with 10.10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Select/Remove All Button
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var navigationBarSelectView: some View {
|
||||||
|
let isAllSelected = selectedTags.count == blockedTags.count
|
||||||
|
|
||||||
|
Button(isAllSelected ? L10n.removeAll : L10n.selectAll) {
|
||||||
|
selectedTags = isAllSelected ? [] : Set(blockedTags)
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.disabled(!isEditing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Delete Selected Confirmation Actions
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var deleteSelectedConfirmationActions: some View {
|
||||||
|
Button(L10n.cancel, role: .cancel) {}
|
||||||
|
|
||||||
|
Button(L10n.delete, role: .destructive) {
|
||||||
|
var tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||||
|
|
||||||
|
for tag in selectedTags {
|
||||||
|
if tag.access {
|
||||||
|
// tempPolicy.allowedTags?.removeAll { $0 == tag.tag }
|
||||||
|
} else {
|
||||||
|
tempPolicy.blockedTags?.removeAll { $0 == tag.tag }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.send(.updatePolicy(tempPolicy))
|
||||||
|
selectedTags.removeAll()
|
||||||
|
isEditing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,23 +109,17 @@ struct ServerUserDetailsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(L10n.parentalControls) {
|
Section(L10n.parentalControls) {
|
||||||
|
ChevronButton(L10n.ratings)
|
||||||
|
.onSelect {
|
||||||
|
router.route(to: \.userParentalRatings, viewModel)
|
||||||
|
}
|
||||||
ChevronButton(L10n.accessSchedules)
|
ChevronButton(L10n.accessSchedules)
|
||||||
.onSelect {
|
.onSelect {
|
||||||
router.route(to: \.userEditAccessSchedules, viewModel)
|
router.route(to: \.userEditAccessSchedules, viewModel)
|
||||||
}
|
}
|
||||||
// TODO: Allow items SDK 10.10 - allowedTags
|
ChevronButton(L10n.accessTags)
|
||||||
/* ChevronButton("Allow items")
|
|
||||||
.onSelect {
|
|
||||||
router.route(to: \.userAllowedTags, viewModel)
|
|
||||||
}
|
|
||||||
// TODO: Block items - blockedTags
|
|
||||||
ChevronButton("Block items")
|
|
||||||
.onSelect {
|
|
||||||
router.route(to: \.userBlockedTags, viewModel)
|
|
||||||
} */
|
|
||||||
ChevronButton(L10n.ratings)
|
|
||||||
.onSelect {
|
.onSelect {
|
||||||
router.route(to: \.userParentalRatings, viewModel)
|
router.route(to: \.userEditAccessTags, viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,21 @@
|
||||||
/// Define the allowed hours for usage and restrict access outside those times.
|
/// Define the allowed hours for usage and restrict access outside those times.
|
||||||
"accessSchedulesDescription" = "Define the allowed hours for usage and restrict access outside those times.";
|
"accessSchedulesDescription" = "Define the allowed hours for usage and restrict access outside those times.";
|
||||||
|
|
||||||
|
/// User will have access to no media unless it contains at least one allowed tag.
|
||||||
|
"accessTagAllowDescription" = "User will have access to no media unless it contains at least one allowed tag.";
|
||||||
|
|
||||||
|
/// Access tag already exists
|
||||||
|
"accessTagAlreadyExists" = "Access tag already exists";
|
||||||
|
|
||||||
|
/// User will have access to all media except when it contains any blocked tag.
|
||||||
|
"accessTagBlockDescription" = "User will have access to all media except when it contains any blocked tag.";
|
||||||
|
|
||||||
|
/// Access Tags
|
||||||
|
"accessTags" = "Access Tags";
|
||||||
|
|
||||||
|
/// Use tags to grant or restrict this user's access to media.
|
||||||
|
"accessTagsDescription" = "Use tags to grant or restrict this user's access to media.";
|
||||||
|
|
||||||
/// Active
|
/// Active
|
||||||
"active" = "Active";
|
"active" = "Active";
|
||||||
|
|
||||||
|
@ -34,8 +49,11 @@
|
||||||
/// Add
|
/// Add
|
||||||
"add" = "Add";
|
"add" = "Add";
|
||||||
|
|
||||||
/// Add Access Schedule
|
/// Add access schedule
|
||||||
"addAccessSchedule" = "Add Access Schedule";
|
"addAccessSchedule" = "Add access schedule";
|
||||||
|
|
||||||
|
/// Add access tag
|
||||||
|
"addAccessTag" = "Add access tag";
|
||||||
|
|
||||||
/// Add API key
|
/// Add API key
|
||||||
"addAPIKey" = "Add API key";
|
"addAPIKey" = "Add API key";
|
||||||
|
@ -88,6 +106,9 @@
|
||||||
/// Allow collection management
|
/// Allow collection management
|
||||||
"allowCollectionManagement" = "Allow collection management";
|
"allowCollectionManagement" = "Allow collection management";
|
||||||
|
|
||||||
|
/// Allowed
|
||||||
|
"allowed" = "Allowed";
|
||||||
|
|
||||||
/// Allow media item deletion
|
/// Allow media item deletion
|
||||||
"allowItemDeletion" = "Allow media item deletion";
|
"allowItemDeletion" = "Allow media item deletion";
|
||||||
|
|
||||||
|
@ -271,6 +292,9 @@
|
||||||
/// bps
|
/// bps
|
||||||
"bitsPerSecond" = "bps";
|
"bitsPerSecond" = "bps";
|
||||||
|
|
||||||
|
/// Blocked
|
||||||
|
"blocked" = "Blocked";
|
||||||
|
|
||||||
/// Block unrated items
|
/// Block unrated items
|
||||||
"blockUnratedItems" = "Block unrated items";
|
"blockUnratedItems" = "Block unrated items";
|
||||||
|
|
||||||
|
@ -1717,6 +1741,9 @@
|
||||||
/// System
|
/// System
|
||||||
"system" = "System";
|
"system" = "System";
|
||||||
|
|
||||||
|
/// Tag
|
||||||
|
"tag" = "Tag";
|
||||||
|
|
||||||
/// Tagline
|
/// Tagline
|
||||||
"tagline" = "Tagline";
|
"tagline" = "Tagline";
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue