Skip to content

Commit

Permalink
[iOS] Admin Dashboard - Users (jellyfin#1287)
Browse files Browse the repository at this point in the history
  • Loading branch information
JPKribs authored Oct 31, 2024
1 parent 9e11901 commit e0990e3
Show file tree
Hide file tree
Showing 27 changed files with 1,730 additions and 257 deletions.
33 changes: 31 additions & 2 deletions Shared/Coordinators/SettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ final class SettingsCoordinator: NavigationCoordinatable {
@Route(.push)
var serverLogs = makeServerLogs
@Route(.push)
var users = makeUsers
@Route(.push)
var userDetails = makeUserDetails
@Route(.push)
var userDevices = makeUserDevices
@Route(.modal)
var addServerUser = makeAddServerUser
@Route(.push)
var apiKeys = makeAPIKeys
// <- End of AdminDashboard Items

Expand Down Expand Up @@ -118,7 +126,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
}

func makeEditCustomDeviceProfile(profile: Binding<CustomDeviceProfile>)
-> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
-> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator>
{
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator(profile: profile))
}

Expand Down Expand Up @@ -232,6 +241,27 @@ final class SettingsCoordinator: NavigationCoordinatable {
ServerLogsView()
}

@ViewBuilder
func makeUsers() -> some View {
ServerUsersView()
}

@ViewBuilder
func makeUserDetails(user: UserDto) -> some View {
ServerUserDetailsView(user: user)
}

func makeAddServerUser() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
AddServerUserView()
}
}

@ViewBuilder
func makeUserDevices() -> some View {
DevicesView()
}

@ViewBuilder
func makeAPIKeys() -> some View {
APIKeysView()
Expand All @@ -245,7 +275,6 @@ final class SettingsCoordinator: NavigationCoordinatable {
DebugSettingsView()
}
#endif

#endif

#if os(tvOS)
Expand Down
24 changes: 24 additions & 0 deletions Shared/Extensions/FormatStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,27 @@ struct TimeIntervalFormatStyle: FormatStyle {
).format(t ..< t.addingTimeInterval(value))
}
}

struct LastSeenFormatStyle: FormatStyle {

func format(_ value: Date?) -> String {

guard let value else {
return L10n.never
}

let timeInterval = Date.now.timeIntervalSince(value)
let twentyFourHours: TimeInterval = 24 * 60 * 60

if timeInterval <= twentyFourHours {
return value.formatted(.relative(presentation: .numeric, unitsStyle: .narrow))
} else {
return value.formatted(Date.FormatStyle.dateTime.year().month().day())
}
}
}

extension FormatStyle where Self == LastSeenFormatStyle {

static var lastSeen: LastSeenFormatStyle { LastSeenFormatStyle() }
}
2 changes: 1 addition & 1 deletion Shared/Extensions/JellyfinAPI/DeviceInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import JellyfinAPI

extension DeviceInfo {

var device: DeviceType {
var type: DeviceType {
DeviceType(
client: appName,
deviceName: name
Expand Down
2 changes: 2 additions & 0 deletions Shared/Extensions/Sequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ extension Sequence {
sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
}

// TODO: a flipped version of `sorted`

/// Returns the elements of the sequence, sorted by comparing values
/// at the given `KeyPath` of `Element`.
///
Expand Down
2 changes: 2 additions & 0 deletions Shared/Extensions/URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ extension URL {

static let jellyfinDocsTasks: URL = URL(string: "https://jellyfin.org/docs/general/server/tasks")!

static let jellyfinDocsUsers: URL = URL(string: "https://jellyfin.org/docs/general/server/users")!

func isDirectoryAndReachable() throws -> Bool {
guard try resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true else {
return false
Expand Down
2 changes: 2 additions & 0 deletions Shared/Services/SwiftfinNotifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,6 @@ extension Notifications.Key {
static let didChangeUserProfileImage = NotificationKey("didChangeUserProfileImage")

static let didStartPlayback = NotificationKey("didStartPlayback")

static let didAddServerUser = NotificationKey("didStartPlayback")
}
42 changes: 42 additions & 0 deletions Shared/Strings/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ internal enum L10n {
internal static let accentColorDescription = L10n.tr("Localizable", "accentColorDescription", fallback: "Some views may need an app restart to update.")
/// Accessibility
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
/// Active
internal static let active = L10n.tr("Localizable", "active", fallback: "Active")
/// ActiveSessionsView Header
internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices")
/// Activity
internal static let activity = L10n.tr("Localizable", "activity", fallback: "Activity")
/// Add
internal static let add = L10n.tr("Localizable", "add", fallback: "Add")
/// Add API key
Expand All @@ -30,8 +34,12 @@ internal enum L10n {
internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add trigger")
/// Add URL
internal static let addURL = L10n.tr("Localizable", "addURL", fallback: "Add URL")
/// Add User
internal static let addUser = L10n.tr("Localizable", "addUser", fallback: "Add User")
/// Administration Dashboard Section
internal static let administration = L10n.tr("Localizable", "administration", fallback: "Administration")
/// Administrator
internal static let administrator = L10n.tr("Localizable", "administrator", fallback: "Administrator")
/// Advanced
internal static let advanced = L10n.tr("Localizable", "advanced", fallback: "Advanced")
/// Airs %s
Expand All @@ -46,6 +54,8 @@ internal enum L10n {
internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media")
/// Select Server View - Select All Servers
internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "All Servers")
/// View and manage all registered users on the server, including their permissions and activity status.
internal static let allUsersDescription = L10n.tr("Localizable", "allUsersDescription", fallback: "View and manage all registered users on the server, including their permissions and activity status.")
/// TranscodeReason - Anamorphic Video Not Supported
internal static let anamorphicVideoNotSupported = L10n.tr("Localizable", "anamorphicVideoNotSupported", fallback: "Anamorphic video is not supported")
/// API Key Copied
Expand Down Expand Up @@ -210,6 +220,8 @@ internal enum L10n {
internal static let confirm = L10n.tr("Localizable", "confirm", fallback: "Confirm")
/// Confirm Close
internal static let confirmClose = L10n.tr("Localizable", "confirmClose", fallback: "Confirm Close")
/// Confirm Password
internal static let confirmPassword = L10n.tr("Localizable", "confirmPassword", fallback: "Confirm Password")
/// Connect
internal static let connect = L10n.tr("Localizable", "connect", fallback: "Connect")
/// Connect Manually
Expand Down Expand Up @@ -290,14 +302,28 @@ internal enum L10n {
internal static let deleteDeviceWarning = L10n.tr("Localizable", "deleteDeviceWarning", fallback: "Are you sure you wish to delete this device? This session will be logged out.")
/// Delete Selected Devices
internal static let deleteSelectedDevices = L10n.tr("Localizable", "deleteSelectedDevices", fallback: "Delete Selected Devices")
/// Delete Selected Users
internal static let deleteSelectedUsers = L10n.tr("Localizable", "deleteSelectedUsers", fallback: "Delete Selected Users")
/// Are you sure you wish to delete all selected devices? All selected sessions will be logged out.
internal static let deleteSelectionDevicesWarning = L10n.tr("Localizable", "deleteSelectionDevicesWarning", fallback: "Are you sure you wish to delete all selected devices? All selected sessions will be logged out.")
/// Are you sure you wish to delete all selected users?
internal static let deleteSelectionUsersWarning = L10n.tr("Localizable", "deleteSelectionUsersWarning", fallback: "Are you sure you wish to delete all selected users?")
/// Server Detail View - Delete Server
internal static let deleteServer = L10n.tr("Localizable", "deleteServer", fallback: "Delete Server")
/// Delete Trigger
internal static let deleteTrigger = L10n.tr("Localizable", "deleteTrigger", fallback: "Delete Trigger")
/// Are you sure you want to delete this trigger? This action cannot be undone.
internal static let deleteTriggerConfirmationMessage = L10n.tr("Localizable", "deleteTriggerConfirmationMessage", fallback: "Are you sure you want to delete this trigger? This action cannot be undone.")
/// Delete User
internal static let deleteUser = L10n.tr("Localizable", "deleteUser", fallback: "Delete User")
/// Failed to Delete User
internal static let deleteUserFailed = L10n.tr("Localizable", "deleteUserFailed", fallback: "Failed to Delete User")
/// Cannot delete a user from the same user (%1$@).
internal static func deleteUserSelfDeletion(_ p1: Any) -> String {
return L10n.tr("Localizable", "deleteUserSelfDeletion", String(describing: p1), fallback: "Cannot delete a user from the same user (%1$@).")
}
/// Are you sure you wish to delete this user?
internal static let deleteUserWarning = L10n.tr("Localizable", "deleteUserWarning", fallback: "Are you sure you wish to delete this user?")
/// Delivery
internal static let delivery = L10n.tr("Localizable", "delivery", fallback: "Delivery")
/// Details
Expand Down Expand Up @@ -338,6 +364,8 @@ internal enum L10n {
internal static let editJumpLengths = L10n.tr("Localizable", "editJumpLengths", fallback: "Edit Jump Lengths")
/// Select Server View - Edit an Existing Server
internal static let editServer = L10n.tr("Localizable", "editServer", fallback: "Edit Server")
/// Edit Users
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")
/// Enabled
Expand Down Expand Up @@ -392,6 +420,8 @@ internal enum L10n {
internal static let grid = L10n.tr("Localizable", "grid", fallback: "Grid")
/// Haptic Feedback
internal static let hapticFeedback = L10n.tr("Localizable", "hapticFeedback", fallback: "Haptic Feedback")
/// Hidden
internal static let hidden = L10n.tr("Localizable", "hidden", fallback: "Hidden")
/// Home
internal static let home = L10n.tr("Localizable", "home", fallback: "Home")
/// Hours
Expand Down Expand Up @@ -522,6 +552,8 @@ internal enum L10n {
internal static let neverRun = L10n.tr("Localizable", "neverRun", fallback: "Never run")
/// News
internal static let news = L10n.tr("Localizable", "news", fallback: "News")
/// New User
internal static let newUser = L10n.tr("Localizable", "newUser", fallback: "New User")
/// Next
internal static let next = L10n.tr("Localizable", "next", fallback: "Next")
/// Next Item
Expand Down Expand Up @@ -580,6 +612,8 @@ internal enum L10n {
internal static let onNow = L10n.tr("Localizable", "onNow", fallback: "On Now")
/// Operating System
internal static let operatingSystem = L10n.tr("Localizable", "operatingSystem", fallback: "Operating System")
/// Options
internal static let options = L10n.tr("Localizable", "options", fallback: "Options")
/// Orange
internal static let orange = L10n.tr("Localizable", "orange", fallback: "Orange")
/// Order
Expand All @@ -602,6 +636,8 @@ internal enum L10n {
}
/// Password
internal static let password = L10n.tr("Localizable", "password", fallback: "Password")
/// New passwords do not match
internal static let passwordsDoNotMatch = L10n.tr("Localizable", "passwordsDoNotMatch", fallback: "New passwords do not match")
/// Video Player Settings View - Pause on Background
internal static let pauseOnBackground = L10n.tr("Localizable", "pauseOnBackground", fallback: "Pause on background")
/// People
Expand Down Expand Up @@ -738,6 +774,8 @@ internal enum L10n {
internal static let retry = L10n.tr("Localizable", "retry", fallback: "Retry")
/// Right
internal static let `right` = L10n.tr("Localizable", "right", fallback: "Right")
/// Role
internal static let role = L10n.tr("Localizable", "role", fallback: "Role")
/// Button label to run a task
internal static let run = L10n.tr("Localizable", "run", fallback: "Run")
/// Status label for when a task is running
Expand Down Expand Up @@ -1014,6 +1052,10 @@ internal enum L10n {
}
/// Username
internal static let username = L10n.tr("Localizable", "username", fallback: "Username")
/// A username is required
internal static let usernameRequired = L10n.tr("Localizable", "usernameRequired", fallback: "A username is required")
/// Users
internal static let users = L10n.tr("Localizable", "users", fallback: "Users")
/// Version
internal static let version = L10n.tr("Localizable", "version", fallback: "Version")
/// Video
Expand Down
93 changes: 93 additions & 0 deletions Shared/ViewModels/AddServerUserViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Combine
import Foundation
import JellyfinAPI
import OrderedCollections
import SwiftUI

final class AddServerUserViewModel: ViewModel, Eventful, Stateful, Identifiable {

// MARK: Event

enum Event {
case createdNewUser(UserDto)
case error(JellyfinAPIError)
}

// MARK: Actions

enum Action: Equatable {
case cancel
case createUser(username: String, password: String)
}

// MARK: - State

enum State: Hashable {
case initial
case creatingUser
case error(JellyfinAPIError)
}

// MARK: Published Values

var events: AnyPublisher<Event, Never> {
eventSubject
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}

@Published
final var state: State = .initial

private var userTask: AnyCancellable?
private var eventSubject: PassthroughSubject<Event, Never> = .init()

// MARK: - Respond to Action

func respond(to action: Action) -> State {
switch action {
case .cancel:
userTask?.cancel()
return .initial
case let .createUser(username, password):
userTask?.cancel()

userTask = Task {
do {
let newUser = try await createUser(username: username, password: password)

await MainActor.run {
state = .initial
eventSubject.send(.createdNewUser(newUser))
}
} catch {
await MainActor.run {
state = .error(.init(error.localizedDescription))
eventSubject.send(.error(.init(error.localizedDescription)))
}
}
}
.asAnyCancellable()

return .creatingUser
}
}

// MARK: - Create User

private func createUser(username: String, password: String) async throws -> UserDto {
let parameters = CreateUserByName(name: username, password: password)
let request = Paths.createUserByName(parameters)
let response = try await userSession.client.send(request)

return response.value
}
}
Loading

0 comments on commit e0990e3

Please sign in to comment.