From 03879621a13303951bed710f948768f42e84cb11 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 6 Dec 2024 19:48:48 -0700 Subject: [PATCH 01/15] Completed! --- .../AdminDashboardCoordinator.swift | 8 + Shared/Strings/Strings.swift | 8 +- Swiftfin.xcodeproj/project.pbxproj | 12 ++ .../DevicesView/Components/DeviceRow.swift | 1 + .../ServerUserAccessView.swift | 2 +- .../ServerUserDetailsView.swift | 17 +- .../ServerUserDeviceAccessView.swift | 178 ++++++++++++++++++ Translations/en.lproj/Localizable.strings | 10 +- 8 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift diff --git a/Shared/Coordinators/AdminDashboardCoordinator.swift b/Shared/Coordinators/AdminDashboardCoordinator.swift index 119241345..2900631cc 100644 --- a/Shared/Coordinators/AdminDashboardCoordinator.swift +++ b/Shared/Coordinators/AdminDashboardCoordinator.swift @@ -52,6 +52,8 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { @Route(.push) var userDetails = makeUserDetails @Route(.modal) + var userDeviceAccess = makeUserDeviceAccess + @Route(.modal) var userMediaAccess = makeUserMediaAccess @Route(.modal) var userPermissions = makeUserPermissions @@ -132,6 +134,12 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { } } + func makeUserDeviceAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator { + ServerUserDeviceAccessView(viewModel: viewModel) + } + } + func makeUserMediaAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator { NavigationViewCoordinator { ServerUserAccessView(viewModel: viewModel) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 8957324b8..2419eb2a0 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -484,6 +484,8 @@ internal enum L10n { internal static let editUsers = L10n.tr("Localizable", "editUsers", fallback: "Edit Users") /// Empty Next Up internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: "Empty Next Up") + /// Enable all devices + internal static let enableAllDevices = L10n.tr("Localizable", "enableAllDevices", fallback: "Enable all devices") /// Enable all libraries internal static let enableAllLibraries = L10n.tr("Localizable", "enableAllLibraries", fallback: "Enable all libraries") /// Enabled @@ -744,8 +746,6 @@ internal enum L10n { internal static let mayResultInPlaybackFailure = L10n.tr("Localizable", "mayResultInPlaybackFailure", fallback: "This setting may result in media failing to start playback.") /// Media internal static let media = L10n.tr("Localizable", "media", fallback: "Media") - /// Media Access - internal static let mediaAccess = L10n.tr("Localizable", "mediaAccess", fallback: "Media Access") /// Media downloads internal static let mediaDownloads = L10n.tr("Localizable", "mediaDownloads", fallback: "Media downloads") /// Media playback @@ -1234,6 +1234,10 @@ internal enum L10n { internal static let storyArc = L10n.tr("Localizable", "storyArc", fallback: "Story Arc") /// Streams internal static let streams = L10n.tr("Localizable", "streams", fallback: "Streams") + /// %@ Access + internal static func stringWithAccess(_ p1: Any) -> String { + return L10n.tr("Localizable", "stringWithAccess", String(describing: p1), fallback: "%@ Access") + } /// STUDIO internal static let studio = L10n.tr("Localizable", "studio", fallback: "STUDIO") /// Studios diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index da6e05af3..8014bd45a 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -91,6 +91,7 @@ 4E5071DB2CFCEC1D003FA2AD /* GenreEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */; }; 4E5071E42CFCEFD3003FA2AD /* AddItemElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */; }; 4E5334A22CD1A28700D59FA8 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */; }; + 4E537A842D03D11200659A1A /* ServerUserDeviceAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */; }; 4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E556AB12D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; @@ -1204,6 +1205,7 @@ 4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenreEditorViewModel.swift; sourceTree = ""; }; 4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemElementView.swift; sourceTree = ""; }; 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; + 4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserDeviceAccessView.swift; sourceTree = ""; }; 4E556AAF2D036F5E00733377 /* UserPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissions.swift; sourceTree = ""; }; 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = ""; }; @@ -2236,6 +2238,14 @@ path = ActionButtons; sourceTree = ""; }; + 4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */ = { + isa = PBXGroup; + children = ( + 4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */, + ); + path = ServerUserDeviceAccessView; + sourceTree = ""; + }; 4E63B9F52C8A5BEF00C25378 /* AdminDashboardView */ = { isa = PBXGroup; children = ( @@ -2253,6 +2263,7 @@ 4E182C9A2C94991800FBEFD5 /* ServerTasksView */, 4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */, 4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */, + 4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */, 4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */, 4EC2B1992CC96E5E00D866BE /* ServerUsersView */, ); @@ -5682,6 +5693,7 @@ E10B1ECA2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */, E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */, 4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */, + 4E537A842D03D11200659A1A /* ServerUserDeviceAccessView.swift in Sources */, 4E35CE692CBED95F00DBD886 /* DayOfWeek.swift in Sources */, E18E01E3288747230022598C /* CompactPortraitScrollView.swift in Sources */, 4E026A8B2CE804E7005471B5 /* ResetUserPasswordView.swift in Sources */, diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift index a8ccaea0e..3ca0556bd 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift +++ b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift @@ -73,6 +73,7 @@ extension DevicesView { HStack { VStack(alignment: .leading) { + // TODO: 10.9 SDK - Enable Nicknames Text(device.name ?? L10n.unknown) .font(.headline) .lineLimit(2) diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift index c389eb9ba..25192ae5a 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift @@ -42,7 +42,7 @@ struct ServerUserAccessView: View { var body: some View { contentView - .navigationTitle(L10n.mediaAccess) + .navigationTitle(L10n.stringWithAccess(L10n.media)) .navigationBarTitleDisplayMode(.inline) .navigationBarCloseButton { router.dismissCoordinator() diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift index 6c5b52d82..8220b5384 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift @@ -45,23 +45,24 @@ struct ServerUserDetailsView: View { router.route(to: \.resetUserPassword, userId) } } + + ChevronButton(L10n.permissions) + .onSelect { + router.route(to: \.userPermissions, viewModel) + } } - Section { - ChevronButton(L10n.mediaAccess) + Section(L10n.access) { + ChevronButton(L10n.media) .onSelect { router.route(to: \.userMediaAccess, viewModel) } - ChevronButton(L10n.permissions) + ChevronButton(L10n.devices) .onSelect { - router.route(to: \.userPermissions, viewModel) + router.route(to: \.userDeviceAccess, viewModel) } - // TODO: Access: enabledFolders & enableAllFolders - - // TODO: Deletion: enableContentDeletion & enableContentDeletionFromFolders - // TODO: Parental: accessSchedules, maxParentalRating, blockUnratedItems, blockedTags, blockUnratedItems & blockedMediaFolders // TODO: Live TV: enabledChannels & enableAllChannels diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift new file mode 100644 index 000000000..20cf52a86 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift @@ -0,0 +1,178 @@ +// +// 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 ServerUserDeviceAccessView: View { + + // MARK: - Environment + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + // MARK: - ViewModel + + @ObservedObject + private var viewModel: ServerUserAdminViewModel + @ObservedObject + private var devicesViewModel = DevicesViewModel() + + // MARK: - State Variables + + @State + private var tempPolicy: UserPolicy + @State + private var error: Error? + @State + private var isPresentingError: Bool = false + + // MARK: - Current Date + + @CurrentDate + private var currentDate: Date + + // MARK: - Initializer + + init(viewModel: ServerUserAdminViewModel) { + self.viewModel = viewModel + self.tempPolicy = viewModel.user.policy ?? UserPolicy() + } + + // MARK: - Body + + var body: some View { + contentView + .navigationTitle(L10n.stringWithAccess(L10n.device)) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + router.dismissCoordinator() + } + .topBarTrailing { + if viewModel.backgroundStates.contains(.updating) { + ProgressView() + } + Button(L10n.save) { + if tempPolicy != viewModel.user.policy { + viewModel.send(.updatePolicy(tempPolicy)) + } + } + .buttonStyle(.toolbarPill) + .disabled(viewModel.user.policy == tempPolicy) + } + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + isPresentingError = true + case .updated: + UIDevice.feedback(.success) + router.dismissCoordinator() + } + } + .alert( + L10n.error.text, + isPresented: $isPresentingError, + presenting: error + ) { _ in + Button(L10n.dismiss, role: .cancel) {} + } message: { error in + Text(error.localizedDescription) + } + .onFirstAppear { + viewModel.send(.loadLibraries(isHidden: false)) + devicesViewModel.send(.getDevices) + } + } + + // MARK: - Content View + + @ViewBuilder + var contentView: some View { + List { + Section(L10n.access) { + Toggle( + L10n.enableAllDevices, + isOn: $tempPolicy.enableAllDevices.coalesce(false) + ) + } + + if tempPolicy.enableAllDevices == false { + Section { + ForEach(devicesViewModel.devices, id: \.self) { device in + Toggle( + isOn: $tempPolicy.enabledDevices + .coalesce([]) + .contains(device.id!) + ) { + HStack { + deviceImage(device) + deviceDetails(device) + } + } + } + } + } + } + } + + // MARK: - Device Details View + + @ViewBuilder + private func deviceDetails(_ device: DeviceInfo) -> some View { + VStack(alignment: .leading) { + + // TODO: 10.9 SDK - Enable Nicknames + Text(device.name ?? L10n.unknown) + .font(.headline) + .lineLimit(1) + .multilineTextAlignment(.leading) + + TextPairView( + leading: L10n.latestWithString(L10n.user), + trailing: device.lastUserName ?? L10n.unknown + ) + .lineLimit(1) + + TextPairView( + leading: L10n.client, + trailing: device.appName ?? L10n.unknown + ) + .lineLimit(1) + + TextPairView( + L10n.lastSeen, + value: Text(device.dateLastActivity, format: .lastSeen) + ) + .id(currentDate) + .monospacedDigit() + } + .font(.subheadline) + .foregroundStyle(.primary, .secondary) + } + + // MARK: - Device Image View + + @ViewBuilder + private func deviceImage(_ device: DeviceInfo) -> some View { + ZStack { + device.type.clientColor + + Image(device.type.image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 40) + } + .squarePosterStyle() + .posterShadow() + .frame(width: 60, height: 60) + .padding(.trailing) + } +} diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index ee9cc9e5b..465f0a1c7 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -1933,9 +1933,13 @@ // Toggle to enable a setting for all Libraries "enableAllLibraries" = "Enable all libraries"; -// Media Access - Section Title -// Section Title for Server User Media Access Editing -"mediaAccess" = "Media Access"; +// Enable All Devices - Toggle +// Toggle to enable a setting for all devices +"enableAllDevices" = "Enable all devices"; + +// Item Access - Section Title +// Section Title for Server User Media / Device Access Editing +"stringWithAccess" = "%@ Access"; // Deletion - Section Description // Section Title for Media Deletion From 75c03fce74cc4297dbb9672ac2245e94432ba692 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 6 Dec 2024 20:08:10 -0700 Subject: [PATCH 02/15] Reorder User Details --- .../ServerUserDetailsView/ServerUserDetailsView.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift index 8220b5384..1b948de6e 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift @@ -45,11 +45,17 @@ struct ServerUserDetailsView: View { router.route(to: \.resetUserPassword, userId) } } + } + Section { ChevronButton(L10n.permissions) .onSelect { router.route(to: \.userPermissions, viewModel) } + + // TODO: Parental: accessSchedules, maxParentalRating, blockUnratedItems, blockedTags, blockUnratedItems & blockedMediaFolders + + // TODO: Live TV: enabledChannels & enableAllChannels } Section(L10n.access) { @@ -62,10 +68,6 @@ struct ServerUserDetailsView: View { .onSelect { router.route(to: \.userDeviceAccess, viewModel) } - - // TODO: Parental: accessSchedules, maxParentalRating, blockUnratedItems, blockedTags, blockUnratedItems & blockedMediaFolders - - // TODO: Live TV: enabledChannels & enableAllChannels } } .navigationTitle(L10n.user) From a42d6062141104f62243d89579684c2d61ff373f Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 6 Dec 2024 20:31:08 -0700 Subject: [PATCH 03/15] Add Advanced Section --- .../ServerUserDetailsView/ServerUserDetailsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift index 1b948de6e..88fcf7ecf 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift @@ -47,7 +47,7 @@ struct ServerUserDetailsView: View { } } - Section { + Section(L10n.advanced) { ChevronButton(L10n.permissions) .onSelect { router.route(to: \.userPermissions, viewModel) From 7420d93b2f7715e46bfad0711f48dc21d2069a09 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 6 Dec 2024 20:41:09 -0700 Subject: [PATCH 04/15] No need to load the libraries since I'm using the devicesViewModel --- .../ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift index 20cf52a86..fc810a2de 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift @@ -87,7 +87,6 @@ struct ServerUserDeviceAccessView: View { Text(error.localizedDescription) } .onFirstAppear { - viewModel.send(.loadLibraries(isHidden: false)) devicesViewModel.send(.getDevices) } } From 8e157f670ef4a7c20a6454e4d82972a4ebbc8fc1 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 6 Dec 2024 23:31:03 -0700 Subject: [PATCH 05/15] Device Access & Media Access vs * Access --- Shared/Strings/Strings.swift | 12 ++++++------ .../ServerUserAccessView/ServerUserAccessView.swift | 2 +- .../ServerUserDeviceAccessView.swift | 2 +- Translations/en.lproj/Localizable.strings | 10 +++++++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 2419eb2a0..0ccd41a3d 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -436,6 +436,8 @@ internal enum L10n { internal static let details = L10n.tr("Localizable", "details", fallback: "Details") /// Device internal static let device = L10n.tr("Localizable", "device", fallback: "Device") + /// Device Access + internal static let deviceAccess = L10n.tr("Localizable", "deviceAccess", fallback: "Device Access") /// Device Profile internal static let deviceProfile = L10n.tr("Localizable", "deviceProfile", fallback: "Device Profile") /// Decide which media plays natively or requires server transcoding for compatibility. @@ -746,6 +748,8 @@ internal enum L10n { internal static let mayResultInPlaybackFailure = L10n.tr("Localizable", "mayResultInPlaybackFailure", fallback: "This setting may result in media failing to start playback.") /// Media internal static let media = L10n.tr("Localizable", "media", fallback: "Media") + /// Media Access + internal static let mediaAccess = L10n.tr("Localizable", "mediaAccess", fallback: "Media Access") /// Media downloads internal static let mediaDownloads = L10n.tr("Localizable", "mediaDownloads", fallback: "Media downloads") /// Media playback @@ -1086,10 +1090,10 @@ internal enum L10n { internal static let run = L10n.tr("Localizable", "run", fallback: "Run") /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") - /// Runtime - internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Run Time internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") + /// Runtime + internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries @@ -1234,10 +1238,6 @@ internal enum L10n { internal static let storyArc = L10n.tr("Localizable", "storyArc", fallback: "Story Arc") /// Streams internal static let streams = L10n.tr("Localizable", "streams", fallback: "Streams") - /// %@ Access - internal static func stringWithAccess(_ p1: Any) -> String { - return L10n.tr("Localizable", "stringWithAccess", String(describing: p1), fallback: "%@ Access") - } /// STUDIO internal static let studio = L10n.tr("Localizable", "studio", fallback: "STUDIO") /// Studios diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift index 25192ae5a..c389eb9ba 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift @@ -42,7 +42,7 @@ struct ServerUserAccessView: View { var body: some View { contentView - .navigationTitle(L10n.stringWithAccess(L10n.media)) + .navigationTitle(L10n.mediaAccess) .navigationBarTitleDisplayMode(.inline) .navigationBarCloseButton { router.dismissCoordinator() diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift index fc810a2de..f8b19df00 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift @@ -49,7 +49,7 @@ struct ServerUserDeviceAccessView: View { var body: some View { contentView - .navigationTitle(L10n.stringWithAccess(L10n.device)) + .navigationTitle(L10n.deviceAccess) .navigationBarTitleDisplayMode(.inline) .navigationBarCloseButton { router.dismissCoordinator() diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 465f0a1c7..d8b0f9229 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -1937,9 +1937,13 @@ // Toggle to enable a setting for all devices "enableAllDevices" = "Enable all devices"; -// Item Access - Section Title -// Section Title for Server User Media / Device Access Editing -"stringWithAccess" = "%@ Access"; +// Media Access - Section Title +// Section Title for Server User Media Access Editing +"mediaAccess" = "Media Access"; + +// Device Access - Section Title +// Section Title for Server User Device Access Editing +"deviceAccess" = "Device Access"; // Deletion - Section Description // Section Title for Media Deletion From c37b857c5dd03315c216943ff44aaa9c65a137cb Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 7 Dec 2024 02:31:18 -0700 Subject: [PATCH 06/15] Live TV Access + Scaffolding --- .../AdminDashboardCoordinator.swift | 10 +- Shared/Strings/Strings.swift | 16 ++- Swiftfin.xcodeproj/project.pbxproj | 26 +++-- .../ServerUserDetailsView.swift | 40 +++++-- .../ServerUserLiveTVAccessView.swift | 106 ++++++++++++++++++ .../ServerUserMediaAccessView.swift} | 2 +- Translations/en.lproj/Localizable.strings | 24 ++++ 7 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift rename Swiftfin/Views/AdminDashboardView/{ServerUserAccessView/ServerUserAccessView.swift => ServerUserMediaAccessView/ServerUserMediaAccessView.swift} (99%) diff --git a/Shared/Coordinators/AdminDashboardCoordinator.swift b/Shared/Coordinators/AdminDashboardCoordinator.swift index 2900631cc..c68c74800 100644 --- a/Shared/Coordinators/AdminDashboardCoordinator.swift +++ b/Shared/Coordinators/AdminDashboardCoordinator.swift @@ -56,6 +56,8 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { @Route(.modal) var userMediaAccess = makeUserMediaAccess @Route(.modal) + var userLiveTVAccess = makeUserLiveTVAccess + @Route(.modal) var userPermissions = makeUserPermissions @Route(.modal) var resetUserPassword = makeResetUserPassword @@ -142,7 +144,13 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { func makeUserMediaAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator { NavigationViewCoordinator { - ServerUserAccessView(viewModel: viewModel) + ServerUserMediaAccessView(viewModel: viewModel) + } + } + + func makeUserLiveTVAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator { + ServerUserLiveTVAccessView(viewModel: viewModel) } } diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 0ccd41a3d..c749212b0 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -196,6 +196,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.") /// bps internal static let bitsPerSecond = L10n.tr("Localizable", "bitsPerSecond", fallback: "bps") + /// Blocked Channels + internal static let blockedChannels = L10n.tr("Localizable", "blockedChannels", fallback: "Blocked Channels") /// Blue internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue") /// Bugs and Features @@ -222,6 +224,8 @@ internal enum L10n { internal static let changeServer = L10n.tr("Localizable", "changeServer", fallback: "Change Server") /// Changes not saved internal static let changesNotSaved = L10n.tr("Localizable", "changesNotSaved", fallback: "Changes not saved") + /// Channel Mode + internal static let channelMode = L10n.tr("Localizable", "channelMode", fallback: "Channel Mode") /// Channels internal static let channels = L10n.tr("Localizable", "channels", fallback: "Channels") /// Chapters @@ -486,12 +490,20 @@ internal enum L10n { internal static let editUsers = L10n.tr("Localizable", "editUsers", fallback: "Edit Users") /// Empty Next Up internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: "Empty Next Up") + /// Enable All Channels + internal static let enableAllChannels = L10n.tr("Localizable", "enableAllChannels", fallback: "Enable All Channels") /// Enable all devices internal static let enableAllDevices = L10n.tr("Localizable", "enableAllDevices", fallback: "Enable all devices") /// Enable all libraries internal static let enableAllLibraries = L10n.tr("Localizable", "enableAllLibraries", fallback: "Enable all libraries") /// Enabled internal static let enabled = L10n.tr("Localizable", "enabled", fallback: "Enabled") + /// Enabled Channels + internal static let enabledChannels = L10n.tr("Localizable", "enabledChannels", fallback: "Enabled Channels") + /// Enable Live TV access + internal static let enableLiveTVAccess = L10n.tr("Localizable", "enableLiveTVAccess", fallback: "Enable Live TV access") + /// Enable Live TV management + internal static let enableLiveTVManagement = L10n.tr("Localizable", "enableLiveTVManagement", fallback: "Enable Live TV management") /// End Date internal static let endDate = L10n.tr("Localizable", "endDate", fallback: "End Date") /// Ended @@ -1090,10 +1102,10 @@ internal enum L10n { internal static let run = L10n.tr("Localizable", "run", fallback: "Run") /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") - /// Run Time - internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Runtime internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") + /// Run Time + internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 8014bd45a..58dc19044 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -92,6 +92,7 @@ 4E5071E42CFCEFD3003FA2AD /* AddItemElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */; }; 4E5334A22CD1A28700D59FA8 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */; }; 4E537A842D03D11200659A1A /* ServerUserDeviceAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */; }; + 4E537A8D2D04410E00659A1A /* ServerUserLiveTVAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E537A8B2D04410E00659A1A /* ServerUserLiveTVAccessView.swift */; }; 4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E556AB12D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; @@ -200,7 +201,7 @@ 4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; }; 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; }; 4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; }; - 4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */; }; + 4EF3D80B2CF7D6670081AD20 /* ServerUserMediaAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserMediaAccessView.swift */; }; 4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */; }; 4EFD172E2CE4182200A4BAC5 /* LearnMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */; }; 4EFE0C7D2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; }; @@ -1206,6 +1207,7 @@ 4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemElementView.swift; sourceTree = ""; }; 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; 4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserDeviceAccessView.swift; sourceTree = ""; }; + 4E537A8B2D04410E00659A1A /* ServerUserLiveTVAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserLiveTVAccessView.swift; sourceTree = ""; }; 4E556AAF2D036F5E00733377 /* UserPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissions.swift; sourceTree = ""; }; 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = ""; }; @@ -1295,7 +1297,7 @@ 4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = ""; }; 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = ""; }; 4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; - 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserAccessView.swift; sourceTree = ""; }; + 4EF3D8092CF7D6670081AD20 /* ServerUserMediaAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserMediaAccessView.swift; sourceTree = ""; }; 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMenu.swift; sourceTree = ""; }; 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreButton.swift; sourceTree = ""; }; 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonKind.swift; sourceTree = ""; }; @@ -2246,6 +2248,14 @@ path = ServerUserDeviceAccessView; sourceTree = ""; }; + 4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */ = { + isa = PBXGroup; + children = ( + 4E537A8B2D04410E00659A1A /* ServerUserLiveTVAccessView.swift */, + ); + path = ServerUserLiveTVAccessView; + sourceTree = ""; + }; 4E63B9F52C8A5BEF00C25378 /* AdminDashboardView */ = { isa = PBXGroup; children = ( @@ -2261,9 +2271,10 @@ 4E90F7622CC72B1F00417C31 /* EditServerTaskView */, 4E35CE622CBED3FF00DBD886 /* ServerLogsView */, 4E182C9A2C94991800FBEFD5 /* ServerTasksView */, - 4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */, 4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */, 4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */, + 4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */, + 4EF3D80A2CF7D6670081AD20 /* ServerUserMediaAccessView */, 4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */, 4EC2B1992CC96E5E00D866BE /* ServerUsersView */, ); @@ -2623,12 +2634,12 @@ path = Components; sourceTree = ""; }; - 4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */ = { + 4EF3D80A2CF7D6670081AD20 /* ServerUserMediaAccessView */ = { isa = PBXGroup; children = ( - 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */, + 4EF3D8092CF7D6670081AD20 /* ServerUserMediaAccessView.swift */, ); - path = ServerUserAccessView; + path = ServerUserMediaAccessView; sourceTree = ""; }; 5310694F2684E7EE00CFFDBA /* VideoPlayer */ = { @@ -5435,6 +5446,7 @@ C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */, 4E661A322CEFE7BC00025C99 /* SeriesStatus.swift in Sources */, E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */, + 4E537A8D2D04410E00659A1A /* ServerUserLiveTVAccessView.swift in Sources */, BD39577E2C1140810078CEF8 /* TransitionSection.swift in Sources */, 4EC2B1A52CC96FA400D866BE /* ServerUserAdminViewModel.swift in Sources */, E1092F4C29106F9F00163F57 /* GestureAction.swift in Sources */, @@ -5844,7 +5856,7 @@ E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */, E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */, E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */, - 4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */, + 4EF3D80B2CF7D6670081AD20 /* ServerUserMediaAccessView.swift in Sources */, E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */, E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */, E133328F2953B71000EE76AB /* DownloadTaskView.swift in Sources */, diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift index 88fcf7ecf..6e337b3b3 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift @@ -31,7 +31,6 @@ struct ServerUserDetailsView: View { var body: some View { List { - // TODO: Replace with Update Profile Picture & Username AdminDashboardView.UserSection( user: viewModel.user, @@ -52,23 +51,50 @@ struct ServerUserDetailsView: View { .onSelect { router.route(to: \.userPermissions, viewModel) } - - // TODO: Parental: accessSchedules, maxParentalRating, blockUnratedItems, blockedTags, blockUnratedItems & blockedMediaFolders - - // TODO: Live TV: enabledChannels & enableAllChannels } Section(L10n.access) { + ChevronButton(L10n.devices) + .onSelect { + router.route(to: \.userDeviceAccess, viewModel) + } + ChevronButton(L10n.media) .onSelect { router.route(to: \.userMediaAccess, viewModel) } - ChevronButton(L10n.devices) + ChevronButton(L10n.liveTV) .onSelect { - router.route(to: \.userDeviceAccess, viewModel) + router.route(to: \.userLiveTVAccess, viewModel) } } + + /* Section("Parental controls") { + // TODO: Allow items SDK 10.10 - allowedTags + ChevronButton("Allow items") + .onSelect { + router.route(to: \.userAllowedTags, viewModel) + } + + // TODO: Block items - blockedTags + ChevronButton("Block items") + .onSelect { + router.route(to: \.userBlockedTags, viewModel) + } + + // TODO: Access Schedules - accessSchedules + ChevronButton("Access schedule") + .onSelect { + router.route(to: \.userAccessSchedules, viewModel) + } + + // TODO: Parental Rating - maxParentalRating, blockUnratedItems + ChevronButton("Parental rating") + .onSelect { + router.route(to: \.userParentalRating, viewModel) + } + } */ } .navigationTitle(L10n.user) .onAppear { diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift new file mode 100644 index 000000000..f0e36f806 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift @@ -0,0 +1,106 @@ +// +// 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 ServerUserLiveTVAccessView: View { + + // MARK: - Environment + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + // MARK: - ViewModel + + @ObservedObject + private var viewModel: ServerUserAdminViewModel + + // MARK: - State Variables + + @State + private var tempPolicy: UserPolicy + @State + private var error: Error? + @State + private var isPresentingError: Bool = false + + // MARK: - Current Date + + @CurrentDate + private var currentDate: Date + + // MARK: - Initializer + + init(viewModel: ServerUserAdminViewModel) { + self.viewModel = viewModel + self.tempPolicy = viewModel.user.policy ?? UserPolicy() + } + + // MARK: - Body + + var body: some View { + contentView + .navigationTitle(L10n.liveTvAccess.capitalized) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + router.dismissCoordinator() + } + .topBarTrailing { + if viewModel.backgroundStates.contains(.updating) { + ProgressView() + } + Button(L10n.save) { + if tempPolicy != viewModel.user.policy { + viewModel.send(.updatePolicy(tempPolicy)) + } + } + .buttonStyle(.toolbarPill) + .disabled(viewModel.user.policy == tempPolicy) + } + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + isPresentingError = true + case .updated: + UIDevice.feedback(.success) + router.dismissCoordinator() + } + } + .alert( + L10n.error.text, + isPresented: $isPresentingError, + presenting: error + ) { _ in + Button(L10n.dismiss, role: .cancel) {} + } message: { error in + Text(error.localizedDescription) + } + } + + // MARK: - Content View + + @ViewBuilder + var contentView: some View { + List { + Section(L10n.access) { + Toggle( + L10n.enableLiveTVAccess, + isOn: $tempPolicy.enableLiveTvAccess.coalesce(false) + ) + Toggle( + L10n.enableLiveTVManagement, + isOn: $tempPolicy.enableLiveTvManagement.coalesce(false) + ) + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserMediaAccessView/ServerUserMediaAccessView.swift similarity index 99% rename from Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift rename to Swiftfin/Views/AdminDashboardView/ServerUserMediaAccessView/ServerUserMediaAccessView.swift index c389eb9ba..602c55513 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserMediaAccessView/ServerUserMediaAccessView.swift @@ -10,7 +10,7 @@ import Defaults import JellyfinAPI import SwiftUI -struct ServerUserAccessView: View { +struct ServerUserMediaAccessView: View { // MARK: - Environment diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index d8b0f9229..ba1366641 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -2048,3 +2048,27 @@ // Translator - Enum // Represents a translator "translator" = "Translator"; + +// Enable Live TV Access - Toggle +// Toggle to allow the user to access Live TV +"enableLiveTVAccess" = "Enable Live TV access"; + +// Enable Live TV Management - Toggle +// Toggle to allow the user to manage Live TV +"enableLiveTVManagement" = "Enable Live TV management"; + +// Enable All Channels - Toggle +// Toggle to allow access to all channels +"enableAllChannels" = "Enable All Channels"; + +// Channel Mode - Picker title +// Title for the mode picker to switch between enabled and blocked channels +"channelMode" = "Channel Mode"; + +// Enabled Channels - Picker option +// Option to edit enabled channels +"enabledChannels" = "Enabled Channels"; + +// Blocked Channels - Picker option +// Option to edit blocked channels +"blockedChannels" = "Blocked Channels"; From 6cbf33d9d2321244be13f07d0198f40a3d9141dc Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 7 Dec 2024 02:37:42 -0700 Subject: [PATCH 07/15] Live TV Access cleanup - Labels mostly --- Shared/Strings/Strings.swift | 18 +++--------- .../ServerUserLiveTVAccessView.swift | 6 ++-- Translations/en.lproj/Localizable.strings | 28 +++---------------- 3 files changed, 11 insertions(+), 41 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index c749212b0..e114fa33c 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -196,8 +196,6 @@ internal enum L10n { internal static let bitrateTestDisclaimer = L10n.tr("Localizable", "bitrateTestDisclaimer", fallback: "Longer tests are more accurate but may result in a delayed playback.") /// bps internal static let bitsPerSecond = L10n.tr("Localizable", "bitsPerSecond", fallback: "bps") - /// Blocked Channels - internal static let blockedChannels = L10n.tr("Localizable", "blockedChannels", fallback: "Blocked Channels") /// Blue internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue") /// Bugs and Features @@ -224,8 +222,6 @@ internal enum L10n { internal static let changeServer = L10n.tr("Localizable", "changeServer", fallback: "Change Server") /// Changes not saved internal static let changesNotSaved = L10n.tr("Localizable", "changesNotSaved", fallback: "Changes not saved") - /// Channel Mode - internal static let channelMode = L10n.tr("Localizable", "channelMode", fallback: "Channel Mode") /// Channels internal static let channels = L10n.tr("Localizable", "channels", fallback: "Channels") /// Chapters @@ -490,20 +486,12 @@ internal enum L10n { internal static let editUsers = L10n.tr("Localizable", "editUsers", fallback: "Edit Users") /// Empty Next Up internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: "Empty Next Up") - /// Enable All Channels - internal static let enableAllChannels = L10n.tr("Localizable", "enableAllChannels", fallback: "Enable All Channels") /// Enable all devices internal static let enableAllDevices = L10n.tr("Localizable", "enableAllDevices", fallback: "Enable all devices") /// Enable all libraries internal static let enableAllLibraries = L10n.tr("Localizable", "enableAllLibraries", fallback: "Enable all libraries") /// Enabled internal static let enabled = L10n.tr("Localizable", "enabled", fallback: "Enabled") - /// Enabled Channels - internal static let enabledChannels = L10n.tr("Localizable", "enabledChannels", fallback: "Enabled Channels") - /// Enable Live TV access - internal static let enableLiveTVAccess = L10n.tr("Localizable", "enableLiveTVAccess", fallback: "Enable Live TV access") - /// Enable Live TV management - internal static let enableLiveTVManagement = L10n.tr("Localizable", "enableLiveTVManagement", fallback: "Enable Live TV management") /// End Date internal static let endDate = L10n.tr("Localizable", "endDate", fallback: "End Date") /// Ended @@ -1102,10 +1090,10 @@ internal enum L10n { internal static let run = L10n.tr("Localizable", "run", fallback: "Run") /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") - /// Runtime - internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Run Time internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") + /// Runtime + internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries @@ -1360,6 +1348,8 @@ internal enum L10n { internal static let tryAgain = L10n.tr("Localizable", "tryAgain", fallback: "Try again") /// TV internal static let tv = L10n.tr("Localizable", "tv", fallback: "TV") + /// TV Access + internal static let tvAccess = L10n.tr("Localizable", "tvAccess", fallback: "TV Access") /// TV Shows internal static let tvShows = L10n.tr("Localizable", "tvShows", fallback: "TV Shows") /// Type diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift index f0e36f806..1d647b643 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift @@ -47,7 +47,7 @@ struct ServerUserLiveTVAccessView: View { var body: some View { contentView - .navigationTitle(L10n.liveTvAccess.capitalized) + .navigationTitle(L10n.tvAccess) .navigationBarTitleDisplayMode(.inline) .navigationBarCloseButton { router.dismissCoordinator() @@ -93,11 +93,11 @@ struct ServerUserLiveTVAccessView: View { List { Section(L10n.access) { Toggle( - L10n.enableLiveTVAccess, + L10n.liveTvAccess, isOn: $tempPolicy.enableLiveTvAccess.coalesce(false) ) Toggle( - L10n.enableLiveTVManagement, + L10n.liveTvRecordingManagement, isOn: $tempPolicy.enableLiveTvManagement.coalesce(false) ) } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index ba1366641..9bf53f231 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -1945,6 +1945,10 @@ // Section Title for Server User Device Access Editing "deviceAccess" = "Device Access"; +// (Live) TV Access - Section Title +// Section Title for Server User Live TV Access Editing +"tvAccess" = "TV Access"; + // Deletion - Section Description // Section Title for Media Deletion "deletion" = "Deletion"; @@ -2048,27 +2052,3 @@ // Translator - Enum // Represents a translator "translator" = "Translator"; - -// Enable Live TV Access - Toggle -// Toggle to allow the user to access Live TV -"enableLiveTVAccess" = "Enable Live TV access"; - -// Enable Live TV Management - Toggle -// Toggle to allow the user to manage Live TV -"enableLiveTVManagement" = "Enable Live TV management"; - -// Enable All Channels - Toggle -// Toggle to allow access to all channels -"enableAllChannels" = "Enable All Channels"; - -// Channel Mode - Picker title -// Title for the mode picker to switch between enabled and blocked channels -"channelMode" = "Channel Mode"; - -// Enabled Channels - Picker option -// Option to edit enabled channels -"enabledChannels" = "Enabled Channels"; - -// Blocked Channels - Picker option -// Option to edit blocked channels -"blockedChannels" = "Blocked Channels"; From f319a768000aa0b064de6037acb32aaddedb7d20 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 9 Dec 2024 09:06:04 -0700 Subject: [PATCH 08/15] re-use DevicesView.DevicesRow. Fix padding so it can work for both. Mirror in ServerUserRow --- Shared/Strings/Strings.swift | 4 +- .../DevicesView/Components/DeviceRow.swift | 40 +++++++---- .../DevicesView/DevicesView.swift | 3 +- .../ServerUserDetailsView.swift | 10 ++- .../ServerUserDeviceAccessView.swift | 71 ++++--------------- .../Components/ServerUsersRow.swift | 4 +- .../ServerUsersView/ServerUsersView.swift | 3 +- 7 files changed, 50 insertions(+), 85 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index e114fa33c..b088b5522 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -1090,10 +1090,10 @@ internal enum L10n { internal static let run = L10n.tr("Localizable", "run", fallback: "Run") /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") - /// Run Time - internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Runtime internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") + /// Run Time + internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift index 3ca0556bd..fbf226de7 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift +++ b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift @@ -30,17 +30,28 @@ extension DevicesView { @CurrentDate private var currentDate: Date - // MARK: - Observed Objects + // MARK: - Properties let device: DeviceInfo let onSelect: () -> Void - let onDelete: () -> Void + let onDelete: (() -> Void)? + + // MARK: - Initializer + + init( + device: DeviceInfo, + onSelect: @escaping () -> Void, + onDelete: (() -> Void)? = nil + ) { + self.device = device + self.onSelect = onSelect + self.onDelete = onDelete + } // MARK: - Label Styling private var labelForegroundStyle: some ShapeStyle { guard isEditing else { return .primary } - return isSelected ? .primary : .secondary } @@ -72,8 +83,6 @@ extension DevicesView { private var rowContent: some View { HStack { VStack(alignment: .leading) { - - // TODO: 10.9 SDK - Enable Nicknames Text(device.name ?? L10n.unknown) .font(.headline) .lineLimit(2) @@ -83,17 +92,20 @@ extension DevicesView { leading: L10n.user, trailing: device.lastUserName ?? L10n.unknown ) + .lineLimit(1) TextPairView( leading: L10n.client, trailing: device.appName ?? L10n.unknown ) + .lineLimit(1) TextPairView( L10n.lastSeen, value: Text(device.dateLastActivity, format: .lastSeen) ) .id(currentDate) + .lineLimit(1) .monospacedDigit() } .font(.subheadline) @@ -126,20 +138,22 @@ extension DevicesView { // MARK: - Body var body: some View { - ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) { + ListRow { deviceImage } content: { rowContent - .padding(.vertical, 8) } .onSelect(perform: onSelect) + .isSeparatorVisible(false) .swipeActions { - Button( - L10n.delete, - systemImage: "trash", - action: onDelete - ) - .tint(.red) + if let onDelete = onDelete { + Button( + L10n.delete, + systemImage: "trash", + action: onDelete + ) + .tint(.red) + } } } } diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift b/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift index 2a463e3e3..62e36d030 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift +++ b/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift @@ -145,8 +145,7 @@ struct DevicesView: View { } .environment(\.isEditing, isEditing) .environment(\.isSelected, selectedDevices.contains(device.id ?? "")) - .listRowSeparator(.hidden) - .listRowInsets(.zero) + .listRowInsets(.edgeInsets) } } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift index 6e337b3b3..b2471effc 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift @@ -58,16 +58,14 @@ struct ServerUserDetailsView: View { .onSelect { router.route(to: \.userDeviceAccess, viewModel) } - - ChevronButton(L10n.media) - .onSelect { - router.route(to: \.userMediaAccess, viewModel) - } - ChevronButton(L10n.liveTV) .onSelect { router.route(to: \.userLiveTVAccess, viewModel) } + ChevronButton(L10n.media) + .onSelect { + router.route(to: \.userMediaAccess, viewModel) + } } /* Section("Parental controls") { diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift index f8b19df00..af2b2d19b 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift @@ -19,9 +19,9 @@ struct ServerUserDeviceAccessView: View { // MARK: - ViewModel - @ObservedObject + @StateObject private var viewModel: ServerUserAdminViewModel - @ObservedObject + @StateObject private var devicesViewModel = DevicesViewModel() // MARK: - State Variables @@ -41,7 +41,7 @@ struct ServerUserDeviceAccessView: View { // MARK: - Initializer init(viewModel: ServerUserAdminViewModel) { - self.viewModel = viewModel + self._viewModel = StateObject(wrappedValue: viewModel) self.tempPolicy = viewModel.user.policy ?? UserPolicy() } @@ -112,8 +112,16 @@ struct ServerUserDeviceAccessView: View { .contains(device.id!) ) { HStack { - deviceImage(device) - deviceDetails(device) + DevicesView.DeviceRow(device: device) { + if let index = tempPolicy.enabledDevices?.firstIndex(of: device.id!) { + tempPolicy.enabledDevices?.remove(at: index) + } else { + if tempPolicy.enabledDevices == nil { + tempPolicy.enabledDevices = [] + } + tempPolicy.enabledDevices?.append(device.id!) + } + } } } } @@ -121,57 +129,4 @@ struct ServerUserDeviceAccessView: View { } } } - - // MARK: - Device Details View - - @ViewBuilder - private func deviceDetails(_ device: DeviceInfo) -> some View { - VStack(alignment: .leading) { - - // TODO: 10.9 SDK - Enable Nicknames - Text(device.name ?? L10n.unknown) - .font(.headline) - .lineLimit(1) - .multilineTextAlignment(.leading) - - TextPairView( - leading: L10n.latestWithString(L10n.user), - trailing: device.lastUserName ?? L10n.unknown - ) - .lineLimit(1) - - TextPairView( - leading: L10n.client, - trailing: device.appName ?? L10n.unknown - ) - .lineLimit(1) - - TextPairView( - L10n.lastSeen, - value: Text(device.dateLastActivity, format: .lastSeen) - ) - .id(currentDate) - .monospacedDigit() - } - .font(.subheadline) - .foregroundStyle(.primary, .secondary) - } - - // MARK: - Device Image View - - @ViewBuilder - private func deviceImage(_ device: DeviceInfo) -> some View { - ZStack { - device.type.clientColor - - Image(device.type.image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 40) - } - .squarePosterStyle() - .posterShadow() - .frame(width: 60, height: 60) - .padding(.trailing) - } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift b/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift index cae56d5f0..af738e900 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift @@ -158,13 +158,13 @@ extension ServerUsersView { // MARK: - Body var body: some View { - ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) { + ListRow { userImage } content: { rowContent - .padding(.vertical, 8) } .onSelect(perform: onSelect) + .isSeparatorVisible(false) .swipeActions { Button( L10n.delete, diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift index 8cf69690c..b06cf55fc 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift @@ -190,8 +190,7 @@ struct ServerUsersView: View { } .environment(\.isEditing, isEditing) .environment(\.isSelected, selectedUsers.contains(userID)) - .listRowSeparator(.hidden) - .listRowInsets(.zero) + .listRowInsets(.edgeInsets) } } } From 77ef2a9467544a563af725b879c4b0d6f8fbd4fb Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 9 Dec 2024 09:25:11 -0700 Subject: [PATCH 09/15] EditItemElementRow needed the same thing --- Shared/Strings/Strings.swift | 4 ++-- Swiftfin.xcodeproj/project.pbxproj | 2 +- .../EditItemElementView/Components/EditItemElementRow.swift | 6 +++--- .../EditItemElementView/EditItemElementView.swift | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index b088b5522..e114fa33c 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -1090,10 +1090,10 @@ internal enum L10n { internal static let run = L10n.tr("Localizable", "run", fallback: "Run") /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") - /// Runtime - internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Run Time internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") + /// Runtime + internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 58dc19044..016d5a203 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2139,8 +2139,8 @@ 4E31EFA22CFFFB410053DFE7 /* EditItemElementView */ = { isa = PBXGroup; children = ( - 4E31EFA42CFFFB670053DFE7 /* EditItemElementView.swift */, 4E31EFA32CFFFB480053DFE7 /* Components */, + 4E31EFA42CFFFB670053DFE7 /* EditItemElementView.swift */, ); path = EditItemElementView; sourceTree = ""; diff --git a/Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift b/Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift index ec624f268..689413fc5 100644 --- a/Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift +++ b/Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift @@ -27,15 +27,15 @@ extension EditItemElementView { // MARK: - Body var body: some View { - ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) { + ListRow { if type == .people { personImage } } content: { rowContent } - .isSeparatorVisible(false) .onSelect(perform: onSelect) + .isSeparatorVisible(false) .swipeActions { Button(L10n.delete, systemImage: "trash", action: onDelete) .tint(.red) @@ -100,7 +100,7 @@ extension EditItemElementView { .posterStyle(.portrait) .posterShadow() .frame(width: 30, height: 90) - .padding(.trailing) + .padding(.horizontal) } } } diff --git a/Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift b/Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift index 21faec635..5e3e6e507 100644 --- a/Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift +++ b/Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift @@ -182,6 +182,7 @@ struct EditItemElementView: View { ) .environment(\.isEditing, isEditing) .environment(\.isSelected, selectedElements.contains(element)) + .listRowInsets(.edgeInsets) } .onMove { source, destination in guard isReordering else { return } From 5d3ba1a7fdaecae9a052c72ea11578133e14562e Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 9 Dec 2024 10:49:26 -0700 Subject: [PATCH 10/15] Fix ServerUserAccessView vs ServerUserMediaAccessView Merge Issue. This whole folder needs a cleanup which I'll move to another PR. --- Swiftfin.xcodeproj/project.pbxproj | 14 +++++++------- .../ServerUserAccessView.swift} | 5 ++++- 2 files changed, 11 insertions(+), 8 deletions(-) rename Swiftfin/Views/AdminDashboardView/{ServerUserMediaAccessView/ServerUserMediaAccessView.swift => ServerUserAccessView/ServerUserAccessView.swift} (96%) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 016d5a203..ff54e8e1e 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -201,7 +201,7 @@ 4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; }; 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; }; 4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; }; - 4EF3D80B2CF7D6670081AD20 /* ServerUserMediaAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserMediaAccessView.swift */; }; + 4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */; }; 4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */; }; 4EFD172E2CE4182200A4BAC5 /* LearnMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */; }; 4EFE0C7D2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; }; @@ -1297,7 +1297,7 @@ 4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = ""; }; 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = ""; }; 4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; - 4EF3D8092CF7D6670081AD20 /* ServerUserMediaAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserMediaAccessView.swift; sourceTree = ""; }; + 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserAccessView.swift; sourceTree = ""; }; 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMenu.swift; sourceTree = ""; }; 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreButton.swift; sourceTree = ""; }; 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonKind.swift; sourceTree = ""; }; @@ -2274,7 +2274,7 @@ 4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */, 4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */, 4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */, - 4EF3D80A2CF7D6670081AD20 /* ServerUserMediaAccessView */, + 4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */, 4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */, 4EC2B1992CC96E5E00D866BE /* ServerUsersView */, ); @@ -2634,12 +2634,12 @@ path = Components; sourceTree = ""; }; - 4EF3D80A2CF7D6670081AD20 /* ServerUserMediaAccessView */ = { + 4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */ = { isa = PBXGroup; children = ( - 4EF3D8092CF7D6670081AD20 /* ServerUserMediaAccessView.swift */, + 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */, ); - path = ServerUserMediaAccessView; + path = ServerUserAccessView; sourceTree = ""; }; 5310694F2684E7EE00CFFDBA /* VideoPlayer */ = { @@ -5856,7 +5856,7 @@ E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */, E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */, E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */, - 4EF3D80B2CF7D6670081AD20 /* ServerUserMediaAccessView.swift in Sources */, + 4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */, E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */, E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */, E133328F2953B71000EE76AB /* DownloadTaskView.swift in Sources */, diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserMediaAccessView/ServerUserMediaAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift similarity index 96% rename from Swiftfin/Views/AdminDashboardView/ServerUserMediaAccessView/ServerUserMediaAccessView.swift rename to Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift index 602c55513..f6926c657 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserMediaAccessView/ServerUserMediaAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift @@ -132,7 +132,10 @@ struct ServerUserMediaAccessView: View { if tempPolicy.enableContentDeletion == false { Section { - ForEach(viewModel.libraries, id: \.id) { library in + ForEach( + viewModel.libraries.filter { $0.collectionType != "boxsets" }, + id: \.id + ) { library in Toggle( library.displayTitle, isOn: $tempPolicy.enableContentDeletionFromFolders From 89649832a3441d865f43536d7f005d948e62ad37 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 9 Dec 2024 12:43:26 -0700 Subject: [PATCH 11/15] Checkbox improvements --- Shared/Components/ListRowCheckbox.swift | 48 +++++++++++++++++++ Swiftfin.xcodeproj/project.pbxproj | 6 +++ .../DevicesView/Components/DeviceRow.swift | 22 ++------- .../ServerUserDeviceAccessView.swift | 24 ++++------ .../Components/ServerUsersRow.swift | 22 ++------- .../SelectUserView/Components/UserRow.swift | 22 ++------- 6 files changed, 72 insertions(+), 72 deletions(-) create mode 100644 Shared/Components/ListRowCheckbox.swift diff --git a/Shared/Components/ListRowCheckbox.swift b/Shared/Components/ListRowCheckbox.swift new file mode 100644 index 000000000..01b01a70a --- /dev/null +++ b/Shared/Components/ListRowCheckbox.swift @@ -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) 2024 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +struct ListRowCheckbox: View { + + @Default(.accentColor) + private var accentColor + + // MARK: - Environment Variables + + @Environment(\.isEditing) + private var isEditing + @Environment(\.isSelected) + private var isSelected + + // MARK: - Body + + @ViewBuilder + var body: some View { + if isEditing, isSelected { + Image(systemName: "checkmark.circle.fill") + .resizable() + .backport + .fontWeight(.bold) + .aspectRatio(1, contentMode: .fit) + .frame(width: 24, height: 24) + .symbolRenderingMode(.palette) + .foregroundStyle(accentColor.overlayColor, accentColor) + + } else if isEditing { + Image(systemName: "circle") + .resizable() + .backport + .fontWeight(.bold) + .aspectRatio(1, contentMode: .fit) + .frame(width: 24, height: 24) + .foregroundStyle(.secondary) + } + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index ff54e8e1e..fb8516d85 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ 4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */; }; 4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; }; 4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; }; + 4E24ECFB2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */; }; + 4E24ECFC2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */; }; 4E2AC4BE2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */; }; 4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */; }; 4E2AC4C22C6C491200DD600D /* AudoCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4C12C6C491200DD600D /* AudoCodec.swift */; }; @@ -1166,6 +1168,7 @@ 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskRow.swift; sourceTree = ""; }; 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeSettingsCoordinator.swift; sourceTree = ""; }; 4E2182E42CAF67EF0094806B /* PlayMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayMethod.swift; sourceTree = ""; }; + 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowCheckbox.swift; sourceTree = ""; }; 4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileAction.swift; sourceTree = ""; }; 4E2AC4C12C6C491200DD600D /* AudoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudoCodec.swift; sourceTree = ""; }; 4E2AC4C42C6C492700DD600D /* MediaContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaContainer.swift; sourceTree = ""; }; @@ -4227,6 +4230,7 @@ E1AD105326D96F5A003E4A08 /* Components */ = { isa = PBXGroup; children = ( + 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */, E102314C2BCF8A7E009D71FC /* AlternateLayoutView.swift */, E104DC952B9E7E29008F506D /* AssertionFailureView.swift */, E18E0203288749200022598C /* BlurView.swift */, @@ -5006,6 +5010,7 @@ E11C15362BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */, E172D3AE2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */, 4EC1C8532C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */, + 4E24ECFC2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */, E11E374D293E7EC9009EF240 /* ItemFields.swift in Sources */, E1575E6E293E77B5001665B1 /* SpecialFeatureType.swift in Sources */, E12CC1C528D12D9B00678D5D /* SeeAllPosterButton.swift in Sources */, @@ -5717,6 +5722,7 @@ 4EB538BD2CE3CCD100EB72D5 /* MediaPlaybackSection.swift in Sources */, 4EBE064D2C7EB6D3004A6C03 /* VideoPlayerType.swift in Sources */, E1366A222C826DA700A36DED /* EditCustomDeviceProfileCoordinator.swift in Sources */, + 4E24ECFB2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */, E1CCF12E28ABF989006CAC9E /* PosterDisplayType.swift in Sources */, E10B1EC12BD9AD6100A92EAF /* V1UserModel.swift in Sources */, E1E7506A2A33E9B400B2C1EE /* RatingsCard.swift in Sources */, diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift index fbf226de7..5c085947a 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift +++ b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift @@ -113,25 +113,9 @@ extension DevicesView { Spacer() - if isEditing, isSelected { - Image(systemName: "checkmark.circle.fill") - .resizable() - .backport - .fontWeight(.bold) - .aspectRatio(1, contentMode: .fit) - .frame(width: 24, height: 24) - .symbolRenderingMode(.palette) - .foregroundStyle(accentColor.overlayColor, accentColor) - - } else if isEditing { - Image(systemName: "circle") - .resizable() - .backport - .fontWeight(.bold) - .aspectRatio(1, contentMode: .fit) - .frame(width: 24, height: 24) - .foregroundStyle(.secondary) - } + ListRowCheckbox() + .environment(\.isEditing, isEditing) + .environment(\.isSelected, isSelected) } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift index af2b2d19b..87d398047 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift @@ -106,24 +106,18 @@ struct ServerUserDeviceAccessView: View { if tempPolicy.enableAllDevices == false { Section { ForEach(devicesViewModel.devices, id: \.self) { device in - Toggle( - isOn: $tempPolicy.enabledDevices - .coalesce([]) - .contains(device.id!) - ) { - HStack { - DevicesView.DeviceRow(device: device) { - if let index = tempPolicy.enabledDevices?.firstIndex(of: device.id!) { - tempPolicy.enabledDevices?.remove(at: index) - } else { - if tempPolicy.enabledDevices == nil { - tempPolicy.enabledDevices = [] - } - tempPolicy.enabledDevices?.append(device.id!) - } + DevicesView.DeviceRow(device: device) { + if let index = tempPolicy.enabledDevices?.firstIndex(of: device.id!) { + tempPolicy.enabledDevices?.remove(at: index) + } else { + if tempPolicy.enabledDevices == nil { + tempPolicy.enabledDevices = [] } + tempPolicy.enabledDevices?.append(device.id!) } } + .environment(\.isEditing, true) + .environment(\.isSelected, tempPolicy.enabledDevices?.contains(device.id ?? "") == true) } } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift b/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift index af738e900..e4f2e73ec 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift @@ -133,25 +133,9 @@ extension ServerUsersView { Spacer() - if isEditing, isSelected { - Image(systemName: "checkmark.circle.fill") - .resizable() - .backport - .fontWeight(.bold) - .aspectRatio(1, contentMode: .fit) - .frame(width: 24, height: 24) - .symbolRenderingMode(.palette) - .foregroundStyle(accentColor.overlayColor, accentColor) - - } else if isEditing { - Image(systemName: "circle") - .resizable() - .backport - .fontWeight(.bold) - .aspectRatio(1, contentMode: .fit) - .frame(width: 24, height: 24) - .foregroundStyle(.secondary) - } + ListRowCheckbox() + .environment(\.isEditing, isEditing) + .environment(\.isSelected, isSelected) } } diff --git a/Swiftfin/Views/SelectUserView/Components/UserRow.swift b/Swiftfin/Views/SelectUserView/Components/UserRow.swift index a272a80c4..4c5d27871 100644 --- a/Swiftfin/Views/SelectUserView/Components/UserRow.swift +++ b/Swiftfin/Views/SelectUserView/Components/UserRow.swift @@ -117,25 +117,9 @@ extension SelectUserView { Spacer() - if isEditing, isSelected { - Image(systemName: "checkmark.circle.fill") - .resizable() - .backport - .fontWeight(.bold) - .aspectRatio(1, contentMode: .fit) - .frame(width: 24, height: 24) - .symbolRenderingMode(.palette) - .foregroundStyle(accentColor.overlayColor, accentColor) - - } else if isEditing { - Image(systemName: "circle") - .resizable() - .backport - .fontWeight(.bold) - .aspectRatio(1, contentMode: .fit) - .frame(width: 24, height: 24) - .foregroundStyle(.secondary) - } + ListRowCheckbox() + .environment(\.isEditing, isEditing) + .environment(\.isSelected, isSelected) } } From a24141f407f132aff740431660c17fbcea1642d9 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 9 Dec 2024 12:59:19 -0700 Subject: [PATCH 12/15] Duplicate Environments --- .../AdminDashboardView/DevicesView/Components/DeviceRow.swift | 2 -- .../ServerUsersView/Components/ServerUsersRow.swift | 2 -- Swiftfin/Views/SelectUserView/Components/UserRow.swift | 2 -- 3 files changed, 6 deletions(-) diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift index 5c085947a..2b40e7151 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift +++ b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift @@ -114,8 +114,6 @@ extension DevicesView { Spacer() ListRowCheckbox() - .environment(\.isEditing, isEditing) - .environment(\.isSelected, isSelected) } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift b/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift index e4f2e73ec..fe006974b 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift @@ -134,8 +134,6 @@ extension ServerUsersView { Spacer() ListRowCheckbox() - .environment(\.isEditing, isEditing) - .environment(\.isSelected, isSelected) } } diff --git a/Swiftfin/Views/SelectUserView/Components/UserRow.swift b/Swiftfin/Views/SelectUserView/Components/UserRow.swift index 4c5d27871..75c696fc3 100644 --- a/Swiftfin/Views/SelectUserView/Components/UserRow.swift +++ b/Swiftfin/Views/SelectUserView/Components/UserRow.swift @@ -118,8 +118,6 @@ extension SelectUserView { Spacer() ListRowCheckbox() - .environment(\.isEditing, isEditing) - .environment(\.isSelected, isSelected) } } From 4f7befbe3cb0920fb98036d8b46c64a8f677b801 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Mon, 9 Dec 2024 13:44:42 -0700 Subject: [PATCH 13/15] inset grouped list style toggle --- Shared/Components/SeparatorVStack.swift | 71 ++++++++++++ Swiftfin.xcodeproj/project.pbxproj | 16 ++- Swiftfin/Components/ListTitleSection.swift | 105 +++++++++++++----- .../ServerUserDeviceAccessView.swift | 6 +- 4 files changed, 167 insertions(+), 31 deletions(-) create mode 100644 Shared/Components/SeparatorVStack.swift diff --git a/Shared/Components/SeparatorVStack.swift b/Shared/Components/SeparatorVStack.swift new file mode 100644 index 000000000..6d0d1fef2 --- /dev/null +++ b/Shared/Components/SeparatorVStack.swift @@ -0,0 +1,71 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +// https://movingparts.io/variadic-views-in-swiftui + +/// An `HStack` that inserts an optional `separator` between views. +/// +/// - Note: Default spacing is removed. The separator view is responsible +/// for spacing. +struct SeparatorVStack: View { + + private var content: () -> Content + private var separator: () -> Separator + + var body: some View { + _VariadicView.Tree(SeparatorVStackLayout(separator: separator)) { + content() + } + } +} + +extension SeparatorVStack { + + init( + @ViewBuilder separator: @escaping () -> Separator, + @ViewBuilder content: @escaping () -> Content + ) { + self.init( + content: content, + separator: separator + ) + } +} + +extension SeparatorVStack { + + struct SeparatorVStackLayout: _VariadicView_UnaryViewRoot { + + var separator: () -> Separator + + @ViewBuilder + func body(children: _VariadicView.Children) -> some View { + + let last = children.last?.id + + localHStack { + ForEach(children) { child in + child + + if child.id != last { + separator() + } + } + } + } + + @ViewBuilder + private func localHStack(@ViewBuilder content: @escaping () -> some View) -> some View { + VStack(spacing: 0) { + content() + } + } + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index fb8516d85..77f30f157 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -1042,6 +1042,8 @@ E1DD20412BE1EB8C00C0DE51 /* AddUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DD20402BE1EB8C00C0DE51 /* AddUserButton.swift */; }; E1DD55372B6EE533007501C0 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DD55362B6EE533007501C0 /* Task.swift */; }; E1DD55382B6EE533007501C0 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DD55362B6EE533007501C0 /* Task.swift */; }; + E1DD95CC2D07876400335494 /* SeparatorVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DD95CB2D07876400335494 /* SeparatorVStack.swift */; }; + E1DD95CD2D07876400335494 /* SeparatorVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DD95CB2D07876400335494 /* SeparatorVStack.swift */; }; E1DE2B4A2B97ECB900F6715F /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DE2B492B97ECB900F6715F /* ErrorView.swift */; }; E1DE64922CC6F0C900E423B6 /* DeviceSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DE64912CC6F0C900E423B6 /* DeviceSection.swift */; }; E1DE84142B9531C1008CCE21 /* OrderedSectionSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DE84132B9531C1008CCE21 /* OrderedSectionSelectorView.swift */; }; @@ -1862,6 +1864,7 @@ E1DC9846296DEFF500982F06 /* FavoriteIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteIndicator.swift; sourceTree = ""; }; E1DD20402BE1EB8C00C0DE51 /* AddUserButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddUserButton.swift; sourceTree = ""; }; E1DD55362B6EE533007501C0 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; + E1DD95CB2D07876400335494 /* SeparatorVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorVStack.swift; sourceTree = ""; }; E1DE2B492B97ECB900F6715F /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; E1DE64912CC6F0C900E423B6 /* DeviceSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSection.swift; sourceTree = ""; }; E1DE84132B9531C1008CCE21 /* OrderedSectionSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedSectionSelectorView.swift; sourceTree = ""; }; @@ -4230,7 +4233,6 @@ E1AD105326D96F5A003E4A08 /* Components */ = { isa = PBXGroup; children = ( - 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */, E102314C2BCF8A7E009D71FC /* AlternateLayoutView.swift */, E104DC952B9E7E29008F506D /* AssertionFailureView.swift */, E18E0203288749200022598C /* BlurView.swift */, @@ -4240,6 +4242,7 @@ E1153DCB2BBB633B00424D36 /* FastSVGView.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */, + 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */, E1D37F472B9C648E00343D2B /* MaxHeightText.swift */, E1DC983F296DEBA500982F06 /* PosterIndicators */, E1FE69A628C29B720021BC93 /* ProgressBar.swift */, @@ -4248,6 +4251,7 @@ E18E01FF288749200022598C /* RowDivider.swift */, E1E1643D28BB074000323B0A /* SelectorView.swift */, E1356E0129A7309D00382563 /* SeparatorHStack.swift */, + E1DD95CB2D07876400335494 /* SeparatorVStack.swift */, E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */, E1A1528928FD22F600600579 /* TextPairView.swift */, E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */, @@ -5308,6 +5312,7 @@ E1CB75702C80E66700217C76 /* CommaStringBuilder.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */, E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */, + E1DD95CC2D07876400335494 /* SeparatorVStack.swift in Sources */, E1575E7C293E77B5001665B1 /* TimerProxy.swift in Sources */, E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */, E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */, @@ -5762,6 +5767,7 @@ E18ACA952A15A3E100BB4F35 /* (null) in Sources */, 4EB1A8CE2C9B2D0800F43898 /* ActiveSessionRow.swift in Sources */, E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */, + E1DD95CD2D07876400335494 /* SeparatorVStack.swift in Sources */, E1DC983D296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */, E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */, E1C925F72887504B002A7A66 /* PanDirectionGestureRecognizer.swift in Sources */, @@ -6242,7 +6248,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -6258,7 +6264,7 @@ ); MARKETING_VERSION = 1.0.0; OTHER_CFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -6282,7 +6288,7 @@ CURRENT_PROJECT_VERSION = 78; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -6298,7 +6304,7 @@ ); MARKETING_VERSION = 1.0.0; OTHER_CFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; diff --git a/Swiftfin/Components/ListTitleSection.swift b/Swiftfin/Components/ListTitleSection.swift index 88b4576b2..752f0d81f 100644 --- a/Swiftfin/Components/ListTitleSection.swift +++ b/Swiftfin/Components/ListTitleSection.swift @@ -67,67 +67,122 @@ extension ListTitleSection { } } -struct InsetGroupedListHeader: View { +/// A view that mimics an inset grouped section, meant to be +/// used as a header for a `List` with `listStyle(.plain)`. +struct InsetGroupedListHeader: View { @Default(.accentColor) private var accentColor - private let title: String - private let description: String? + private let content: () -> Content + private let title: Text? + private let description: Text? private let onLearnMore: (() -> Void)? - var body: some View { + @ViewBuilder + private var header: some View { Button { onLearnMore?() } label: { - ZStack { - RoundedRectangle(cornerRadius: 16) - .fill(Color.secondarySystemBackground) - - VStack(alignment: .center, spacing: 10) { + VStack(alignment: .center, spacing: 10) { - Text(title) + if let title { + title .font(.title3) .fontWeight(.semibold) + } - if let description { - Text(description) - .multilineTextAlignment(.center) - } + if let description { + description + .multilineTextAlignment(.center) + } - if onLearnMore != nil { - Text("Learn More\u{2026}") - .foregroundStyle(accentColor) - } + if onLearnMore != nil { + Text("Learn More\u{2026}") + .foregroundStyle(accentColor) } - .font(.subheadline) - .frame(maxWidth: .infinity) - .padding(16) } + .font(.subheadline) + .frame(maxWidth: .infinity) + .padding(16) } .foregroundStyle(.primary, .secondary) } + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 16) + .fill(Color.secondarySystemBackground) + + SeparatorVStack { + RowDivider() + } content: { + if title != nil || description != nil { + header + } + + content() + .listRowSeparator(.hidden) + .padding(.init(vertical: 5, horizontal: 20)) + .listRowInsets(.init(vertical: 10, horizontal: 20)) + } + } + } } extension InsetGroupedListHeader { init( - _ title: String, - description: String? = nil + _ title: String? = nil, + description: String? = nil, + onLearnMore: (() -> Void)? = nil, + @ViewBuilder content: @escaping () -> Content + ) { + self.init( + content: content, + title: title == nil ? nil : Text(title!), + description: description == nil ? nil : Text(description!), + onLearnMore: onLearnMore + ) + } + + init( + title: Text, + description: Text? = nil, + onLearnMore: (() -> Void)? = nil, + @ViewBuilder content: @escaping () -> Content ) { self.init( + content: content, title: title, description: description, - onLearnMore: nil + onLearnMore: onLearnMore ) } +} + +extension InsetGroupedListHeader where Content == EmptyView { init( _ title: String, description: String? = nil, - onLearnMore: @escaping () -> Void + onLearnMore: (() -> Void)? = nil + ) { + self.init( + content: { EmptyView() }, + title: Text(title), + description: description == nil ? nil : Text(description!), + onLearnMore: onLearnMore + ) + } + + init( + title: Text, + description: Text? = nil, + onLearnMore: (() -> Void)? = nil ) { self.init( + content: { EmptyView() }, title: title, description: description, onLearnMore: onLearnMore diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift index 87d398047..af7a43763 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift @@ -96,12 +96,15 @@ struct ServerUserDeviceAccessView: View { @ViewBuilder var contentView: some View { List { - Section(L10n.access) { + InsetGroupedListHeader { Toggle( L10n.enableAllDevices, isOn: $tempPolicy.enableAllDevices.coalesce(false) ) } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .padding(.vertical, 24) if tempPolicy.enableAllDevices == false { Section { @@ -122,5 +125,6 @@ struct ServerUserDeviceAccessView: View { } } } + .listStyle(.plain) } } From 556e5c15afd194d72b0f8630f2249acb06dbde78 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 9 Dec 2024 13:59:56 -0700 Subject: [PATCH 14/15] Reset development team --- Shared/Strings/Strings.swift | 4 ++-- Swiftfin.xcodeproj/project.pbxproj | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index e114fa33c..b088b5522 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -1090,10 +1090,10 @@ internal enum L10n { internal static let run = L10n.tr("Localizable", "run", fallback: "Run") /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") - /// Run Time - internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Runtime internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") + /// Run Time + internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 77f30f157..f73c9b960 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -6248,7 +6248,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -6264,7 +6264,7 @@ ); MARKETING_VERSION = 1.0.0; OTHER_CFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -6288,7 +6288,7 @@ CURRENT_PROJECT_VERSION = 78; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -6304,7 +6304,7 @@ ); MARKETING_VERSION = 1.0.0; OTHER_CFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; From ae8da05d5d3606963cf191a2cfee491f493398d0 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 9 Dec 2024 14:02:44 -0700 Subject: [PATCH 15/15] Runtime vs Run Time: https://english.stackexchange.com/questions/67013/runtime-run-time-and-run-time --- Shared/Strings/Strings.swift | 2 -- .../EditMetadataView/Components/Sections/SeriesSection.swift | 2 +- Translations/en.lproj/Localizable.strings | 4 ---- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index b088b5522..b182cf92b 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -1092,8 +1092,6 @@ internal enum L10n { internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") /// Runtime internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") - /// Run Time - internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift index 8f22a559a..5444f1aab 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift @@ -100,7 +100,7 @@ extension EditMetadataView { @ViewBuilder private var runTimeView: some View { ChevronAlertButton( - L10n.runTime, + L10n.runtime, subtitle: ServerTicks(item.runTimeTicks ?? 0) .seconds.formatted(.hourMinute), description: L10n.episodeRuntimeDescription diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 9bf53f231..25ebb8005 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -1817,10 +1817,6 @@ // Label for selecting the air time of an episode. "airTime" = "Air Time"; -// Run Time - Label for runtime input field -// Label for specifying episode runtime. -"runTime" = "Run Time"; - // Episode Runtime Description - Description for runtime input // Description displayed below runtime input for episodes. "episodeRuntimeDescription" = "Episode runtime in minutes.";