[iOS] Admin Dashboard - User Device & TV Access (#1342)
This commit is contained in:
parent
c8acd780be
commit
174487a220
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Content: View, Separator: View>: 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,8 +52,12 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
|||
@Route(.push)
|
||||
var userDetails = makeUserDetails
|
||||
@Route(.modal)
|
||||
var userDeviceAccess = makeUserDeviceAccess
|
||||
@Route(.modal)
|
||||
var userMediaAccess = makeUserMediaAccess
|
||||
@Route(.modal)
|
||||
var userLiveTVAccess = makeUserLiveTVAccess
|
||||
@Route(.modal)
|
||||
var userPermissions = makeUserPermissions
|
||||
@Route(.modal)
|
||||
var resetUserPassword = makeResetUserPassword
|
||||
|
@ -132,9 +136,21 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
|||
}
|
||||
}
|
||||
|
||||
func makeUserDeviceAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
ServerUserDeviceAccessView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
func makeUserMediaAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
ServerUserAccessView(viewModel: viewModel)
|
||||
ServerUserMediaAccessView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
func makeUserLiveTVAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
ServerUserLiveTVAccessView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -484,6 +486,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
|
||||
|
@ -1086,8 +1090,6 @@ 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")
|
||||
/// Save
|
||||
|
@ -1344,6 +1346,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
|
||||
|
|
|
@ -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 */; };
|
||||
|
@ -91,6 +93,8 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
|
@ -1045,6 +1049,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 */; };
|
||||
|
@ -1171,6 +1177,7 @@
|
|||
4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskRow.swift; sourceTree = "<group>"; };
|
||||
4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeSettingsCoordinator.swift; sourceTree = "<group>"; };
|
||||
4E2182E42CAF67EF0094806B /* PlayMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayMethod.swift; sourceTree = "<group>"; };
|
||||
4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowCheckbox.swift; sourceTree = "<group>"; };
|
||||
4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileAction.swift; sourceTree = "<group>"; };
|
||||
4E2AC4C12C6C491200DD600D /* AudoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudoCodec.swift; sourceTree = "<group>"; };
|
||||
4E2AC4C42C6C492700DD600D /* MediaContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaContainer.swift; sourceTree = "<group>"; };
|
||||
|
@ -1211,6 +1218,8 @@
|
|||
4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenreEditorViewModel.swift; sourceTree = "<group>"; };
|
||||
4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemElementView.swift; sourceTree = "<group>"; };
|
||||
4E5334A12CD1A28400D59FA8 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
|
||||
4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserDeviceAccessView.swift; sourceTree = "<group>"; };
|
||||
4E537A8B2D04410E00659A1A /* ServerUserLiveTVAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserLiveTVAccessView.swift; sourceTree = "<group>"; };
|
||||
4E556AAF2D036F5E00733377 /* UserPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissions.swift; sourceTree = "<group>"; };
|
||||
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
||||
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1863,6 +1872,7 @@
|
|||
E1DC9846296DEFF500982F06 /* FavoriteIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteIndicator.swift; sourceTree = "<group>"; };
|
||||
E1DD20402BE1EB8C00C0DE51 /* AddUserButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddUserButton.swift; sourceTree = "<group>"; };
|
||||
E1DD55362B6EE533007501C0 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = "<group>"; };
|
||||
E1DD95CB2D07876400335494 /* SeparatorVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorVStack.swift; sourceTree = "<group>"; };
|
||||
E1DE2B492B97ECB900F6715F /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
E1DE64912CC6F0C900E423B6 /* DeviceSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSection.swift; sourceTree = "<group>"; };
|
||||
E1DE84132B9531C1008CCE21 /* OrderedSectionSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedSectionSelectorView.swift; sourceTree = "<group>"; };
|
||||
|
@ -2148,8 +2158,8 @@
|
|||
4E31EFA22CFFFB410053DFE7 /* EditItemElementView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E31EFA42CFFFB670053DFE7 /* EditItemElementView.swift */,
|
||||
4E31EFA32CFFFB480053DFE7 /* Components */,
|
||||
4E31EFA42CFFFB670053DFE7 /* EditItemElementView.swift */,
|
||||
);
|
||||
path = EditItemElementView;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2249,6 +2259,22 @@
|
|||
path = ActionButtons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */,
|
||||
);
|
||||
path = ServerUserDeviceAccessView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E537A8B2D04410E00659A1A /* ServerUserLiveTVAccessView.swift */,
|
||||
);
|
||||
path = ServerUserLiveTVAccessView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E63B9F52C8A5BEF00C25378 /* AdminDashboardView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2264,8 +2290,10 @@
|
|||
4E90F7622CC72B1F00417C31 /* EditServerTaskView */,
|
||||
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
||||
4E182C9A2C94991800FBEFD5 /* ServerTasksView */,
|
||||
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */,
|
||||
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
||||
4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */,
|
||||
4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */,
|
||||
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */,
|
||||
4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */,
|
||||
4EC2B1992CC96E5E00D866BE /* ServerUsersView */,
|
||||
);
|
||||
|
@ -4228,6 +4256,7 @@
|
|||
E1153DCB2BBB633B00424D36 /* FastSVGView.swift */,
|
||||
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
||||
4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */,
|
||||
4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */,
|
||||
E1D37F472B9C648E00343D2B /* MaxHeightText.swift */,
|
||||
E1DC983F296DEBA500982F06 /* PosterIndicators */,
|
||||
E1FE69A628C29B720021BC93 /* ProgressBar.swift */,
|
||||
|
@ -4236,6 +4265,7 @@
|
|||
E18E01FF288749200022598C /* RowDivider.swift */,
|
||||
E1E1643D28BB074000323B0A /* SelectorView.swift */,
|
||||
E1356E0129A7309D00382563 /* SeparatorHStack.swift */,
|
||||
E1DD95CB2D07876400335494 /* SeparatorVStack.swift */,
|
||||
E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */,
|
||||
E1A1528928FD22F600600579 /* TextPairView.swift */,
|
||||
E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */,
|
||||
|
@ -5005,6 +5035,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 */,
|
||||
|
@ -5302,6 +5333,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 */,
|
||||
|
@ -5446,6 +5478,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 */,
|
||||
|
@ -5704,6 +5737,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 */,
|
||||
|
@ -5715,6 +5749,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 */,
|
||||
|
@ -5754,6 +5789,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 */,
|
||||
|
|
|
@ -67,31 +67,33 @@ 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<Content: View>: 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) {
|
||||
|
||||
Text(title)
|
||||
if let title {
|
||||
title
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
if let description {
|
||||
Text(description)
|
||||
description
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
|
@ -104,30 +106,83 @@ struct InsetGroupedListHeader: View {
|
|||
.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(
|
||||
title: title,
|
||||
description: description,
|
||||
onLearnMore: nil
|
||||
content: content,
|
||||
title: title == nil ? nil : Text(title!),
|
||||
description: description == nil ? nil : Text(description!),
|
||||
onLearnMore: onLearnMore
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
_ title: String,
|
||||
description: String? = nil,
|
||||
onLearnMore: @escaping () -> Void
|
||||
title: Text,
|
||||
description: Text? = nil,
|
||||
onLearnMore: (() -> Void)? = nil,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) {
|
||||
self.init(
|
||||
content: content,
|
||||
title: title,
|
||||
description: description,
|
||||
onLearnMore: onLearnMore
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension InsetGroupedListHeader where Content == EmptyView {
|
||||
|
||||
init(
|
||||
_ title: String,
|
||||
description: String? = nil,
|
||||
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
|
||||
|
|
|
@ -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,7 +83,6 @@ extension DevicesView {
|
|||
private var rowContent: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Text(device.name ?? L10n.unknown)
|
||||
.font(.headline)
|
||||
.lineLimit(2)
|
||||
|
@ -82,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)
|
||||
|
@ -100,39 +113,22 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if let onDelete = onDelete {
|
||||
Button(
|
||||
L10n.delete,
|
||||
systemImage: "trash",
|
||||
|
@ -143,3 +139,4 @@ extension DevicesView {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,8 +145,7 @@ struct DevicesView: View {
|
|||
}
|
||||
.environment(\.isEditing, isEditing)
|
||||
.environment(\.isSelected, selectedDevices.contains(device.id ?? ""))
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(.zero)
|
||||
.listRowInsets(.edgeInsets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import Defaults
|
|||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct ServerUserAccessView: View {
|
||||
struct ServerUserMediaAccessView: View {
|
||||
|
||||
// MARK: - Environment
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@ struct ServerUserDetailsView: View {
|
|||
|
||||
var body: some View {
|
||||
List {
|
||||
|
||||
// TODO: Replace with Update Profile Picture & Username
|
||||
AdminDashboardView.UserSection(
|
||||
user: viewModel.user,
|
||||
|
@ -47,25 +46,53 @@ struct ServerUserDetailsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
ChevronButton(L10n.mediaAccess)
|
||||
.onSelect {
|
||||
router.route(to: \.userMediaAccess, viewModel)
|
||||
}
|
||||
|
||||
Section(L10n.advanced) {
|
||||
ChevronButton(L10n.permissions)
|
||||
.onSelect {
|
||||
router.route(to: \.userPermissions, viewModel)
|
||||
}
|
||||
|
||||
// TODO: Access: enabledFolders & enableAllFolders
|
||||
|
||||
// TODO: Deletion: enableContentDeletion & enableContentDeletionFromFolders
|
||||
|
||||
// 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.liveTV)
|
||||
.onSelect {
|
||||
router.route(to: \.userLiveTVAccess, viewModel)
|
||||
}
|
||||
ChevronButton(L10n.media)
|
||||
.onSelect {
|
||||
router.route(to: \.userMediaAccess, 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 {
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
@StateObject
|
||||
private var viewModel: ServerUserAdminViewModel
|
||||
@StateObject
|
||||
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 = StateObject(wrappedValue: viewModel)
|
||||
self.tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
contentView
|
||||
.navigationTitle(L10n.deviceAccess)
|
||||
.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 {
|
||||
devicesViewModel.send(.getDevices)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Content View
|
||||
|
||||
@ViewBuilder
|
||||
var contentView: some View {
|
||||
List {
|
||||
InsetGroupedListHeader {
|
||||
Toggle(
|
||||
L10n.enableAllDevices,
|
||||
isOn: $tempPolicy.enableAllDevices.coalesce(false)
|
||||
)
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.padding(.vertical, 24)
|
||||
|
||||
if tempPolicy.enableAllDevices == false {
|
||||
Section {
|
||||
ForEach(devicesViewModel.devices, id: \.self) { device in
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
|
@ -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.tvAccess)
|
||||
.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.liveTvAccess,
|
||||
isOn: $tempPolicy.enableLiveTvAccess.coalesce(false)
|
||||
)
|
||||
Toggle(
|
||||
L10n.liveTvRecordingManagement,
|
||||
isOn: $tempPolicy.enableLiveTvManagement.coalesce(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -133,38 +133,20 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
|
|
@ -189,8 +189,7 @@ struct ServerUsersView: View {
|
|||
}
|
||||
.environment(\.isEditing, isEditing)
|
||||
.environment(\.isSelected, selectedUsers.contains(userID))
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(.zero)
|
||||
.listRowInsets(.edgeInsets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,6 +182,7 @@ struct EditItemElementView<Element: Hashable>: View {
|
|||
)
|
||||
.environment(\.isEditing, isEditing)
|
||||
.environment(\.isSelected, selectedElements.contains(element))
|
||||
.listRowInsets(.edgeInsets)
|
||||
}
|
||||
.onMove { source, destination in
|
||||
guard isReordering else { return }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -117,25 +117,7 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.";
|
||||
|
@ -1933,10 +1929,22 @@
|
|||
// Toggle to enable a setting for all Libraries
|
||||
"enableAllLibraries" = "Enable all libraries";
|
||||
|
||||
// Enable All Devices - Toggle
|
||||
// Toggle to enable a setting for all devices
|
||||
"enableAllDevices" = "Enable all devices";
|
||||
|
||||
// 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";
|
||||
|
||||
// (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";
|
||||
|
|
Loading…
Reference in New Issue