Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Admin Dashboard - User Profiles #1328

Merged
merged 30 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
777428a
Make user profile more generic. Still need to make it work for the re…
JPKribs Dec 12, 2024
b36e9b9
Username Changing and PFP deletion.
JPKribs Dec 13, 2024
a4b4695
Merge branch 'main' into adminDashboardProfile
JPKribs Dec 14, 2024
ebf4e1b
Functional, refreshing, and good to go!
JPKribs Dec 14, 2024
e9e3bcb
Clean up localizations
JPKribs Dec 14, 2024
82c6629
Migrate [UserDto] -> IdentifiedArrayOf<UserDto>
JPKribs Dec 18, 2024
214a244
Solve "Username should probably be at the top of this section."
JPKribs Dec 18, 2024
121f5b7
allow notification filter
LePips Dec 18, 2024
896def1
WIP:
JPKribs Dec 19, 2024
0d1265b
Centralize UserProfileHeroImages
JPKribs Dec 19, 2024
064949b
Rename UserProfileImages
JPKribs Dec 19, 2024
b9d21c9
Merge branch 'main' into adminDashboardProfile
JPKribs Dec 19, 2024
275c625
Fix Merge Issue?
JPKribs Dec 19, 2024
ff97185
Merge remote-tracking branch 'refs/remotes/origin/adminDashboardProfile'
JPKribs Dec 19, 2024
6c48ac0
Move to UserProfileImage
JPKribs Dec 19, 2024
e3192a2
Merge branch 'main' into adminDashboardProfile
JPKribs Dec 19, 2024
95b163f
Merge branch 'main' into adminDashboardProfile
JPKribs Dec 20, 2024
157334f
Merge with Main
JPKribs Dec 20, 2024
d14b25b
Merge branch 'main' into adminDashboardProfile
JPKribs Dec 21, 2024
17acf96
Fix Merge?
JPKribs Dec 21, 2024
a99bdcd
Clear the cache on update.
JPKribs Dec 23, 2024
38f7656
Delete duplicate `UserProfileImage`
JPKribs Dec 23, 2024
7c4a17d
Merge branch 'jellyfin:main' into adminDashboardProfile
JPKribs Dec 28, 2024
04cfe96
wip
LePips Dec 28, 2024
e9df707
Merge branch 'fix-image-caching' into user-images-cache-fixes
LePips Dec 28, 2024
e69e33b
wip
LePips Dec 29, 2024
64ecc09
Update ImagePipeline.swift
LePips Dec 29, 2024
77f8e7e
fix tvOS build issue and update comment to be more accurate
JPKribs Dec 29, 2024
3579d27
clean up
LePips Dec 29, 2024
b71c6e8
fix string
LePips Dec 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions RedrawOnNotificationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,25 @@ struct RedrawOnNotificationView<Content: View, P>: View {
@State
private var id = 0

private let filter: (P) -> Bool
private let key: Notifications.Key<P>
private let content: () -> Content

init(_ key: Notifications.Key<P>, @ViewBuilder content: @escaping () -> Content) {
init(
_ key: Notifications.Key<P>,
filter: @escaping (P) -> Bool = { _ in true },
@ViewBuilder content: @escaping () -> Content
) {
self.filter = filter
self.key = key
self.content = content
}

var body: some View {
content()
.id(id)
.onNotification(key) { _ in
.onNotification(key) { p in
guard filter(p) else { return }
id += 1
}
}
Expand Down
115 changes: 115 additions & 0 deletions Shared/Components/UserProfileImage/UserEditableHeroImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// 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 Factory
import JellyfinAPI
import Nuke
import SwiftUI

struct UserEditableHeroImage: View {

// MARK: - Accent Color

@Default(.accentColor)
private var accentColor

// MARK: - User Session

@Injected(\.currentUserSession)
private var userSession

// MARK: - User Variables

private let user: UserDto
private let source: ImageSource
private let pipeline: ImagePipeline

// MARK: - User Actions

private let onUpdate: () -> Void
private let onDelete: () -> Void

// MARK: - Dialog State

@State
private var isPresentingOptions: Bool = false

// MARK: - Initializer

init(
user: UserDto,
source: ImageSource,
pipeline: ImagePipeline = .Swiftfin.default,
onUpdate: @escaping () -> Void,
onDelete: @escaping () -> Void
) {
self.user = user
self.source = source
self.pipeline = pipeline
self.onUpdate = onUpdate
self.onDelete = onDelete
}

// MARK: - Body

var body: some View {
Section {
VStack(alignment: .center) {
Button {
isPresentingOptions = true
} label: {
ZStack(alignment: .bottomTrailing) {
// `.aspectRatio(contentMode: .fill)` on `imageView` alone
// causes a crash on some iOS versions
ZStack {
UserProfileHeroImage(
userId: user.id,
source: source,
pipeline: userSession?.user.id == user.id ? .Swiftfin.branding : .Swiftfin.default
)
}
.aspectRatio(1, contentMode: .fill)
.clipShape(.circle)
.frame(width: 150, height: 150)
.shadow(radius: 5)

Image(systemName: "pencil.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.shadow(radius: 10)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
}
}

Text(user.name ?? L10n.unknown)
.fontWeight(.semibold)
.font(.title2)
}
.frame(maxWidth: .infinity)
.listRowBackground(Color.clear)
}
.confirmationDialog(
"""
\(L10n.profileImage)
\(L10n.viewsMayRequireRestart)
""",
isPresented: $isPresentingOptions,
titleVisibility: .visible
) {
Text(L10n.viewsMayRequireRestart)
Button(L10n.selectImage) {
onUpdate()
}
Button(L10n.delete, role: .destructive) {
onDelete()
}
}
}
}
63 changes: 63 additions & 0 deletions Shared/Components/UserProfileImage/UserProfileHeroImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// 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 Nuke
import SwiftUI

struct UserProfileHeroImage: View {

// MARK: - User Variables

private let userId: String?
private let source: ImageSource
private let pipeline: ImagePipeline
private let placeholder: any View

// MARK: - Initializer

init(
userId: String?,
source: ImageSource,
pipeline: ImagePipeline = .Swiftfin.default,
placeholder: any View = SystemImageContentView(systemName: "person.fill", ratio: 0.5)
) {
self.userId = userId
self.source = source
self.pipeline = pipeline
self.placeholder = placeholder
}

// MARK: - Body

var body: some View {
RedrawOnNotificationView(
.didChangeUserProfile,
filter: {
$0 == userId
}
) {
ImageView(source)
.pipeline(pipeline)
.image {
$0.posterBorder(ratio: 1 / 2, of: \.width)
}
.placeholder { _ in
placeholder
}
.failure {
placeholder
}
.posterShadow()
.aspectRatio(1, contentMode: .fill)
.clipShape(Circle())
.shadow(radius: 5)
}
}
}
6 changes: 6 additions & 0 deletions Shared/Coordinators/AdminDashboardCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
var userEditAccessSchedules = makeUserEditAccessSchedules
@Route(.modal)
var userAddAccessSchedule = makeUserAddAccessSchedule
@Route(.modal)
var userPhotoPicker = makeUserPhotoPicker

// MARK: - Route: API Keys

Expand Down Expand Up @@ -139,6 +141,10 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
ServerUserDetailsView(user: user)
}

func makeUserPhotoPicker(viewModel: UserProfileImageViewModel) -> NavigationViewCoordinator<UserProfileImageCoordinator> {
NavigationViewCoordinator(UserProfileImageCoordinator(viewModel: viewModel))
}

func makeAddServerUser() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
AddServerUserView()
Expand Down
4 changes: 2 additions & 2 deletions Shared/Coordinators/SettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
UserLocalSecurityView()
}

func makePhotoPicker(viewModel: SettingsViewModel) -> NavigationViewCoordinator<UserProfileImageCoordinator> {
NavigationViewCoordinator(UserProfileImageCoordinator())
func makePhotoPicker(viewModel: UserProfileImageViewModel) -> NavigationViewCoordinator<UserProfileImageCoordinator> {
NavigationViewCoordinator(UserProfileImageCoordinator(viewModel: viewModel))
}

@ViewBuilder
Expand Down
23 changes: 19 additions & 4 deletions Shared/Coordinators/UserProfileImageCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,34 @@ import SwiftUI

final class UserProfileImageCoordinator: NavigationCoordinatable {

// MARK: - Navigation Components

let stack = Stinsen.NavigationStack(initial: \UserProfileImageCoordinator.start)

@Root
var start = makeStart

// MARK: - Routes

@Route(.push)
var cropImage = makeCropImage

// MARK: - Observed Object

@ObservedObject
var viewModel: UserProfileImageViewModel

// MARK: - Initializer

init(viewModel: UserProfileImageViewModel) {
self.viewModel = viewModel
}

// MARK: - Views

func makeCropImage(image: UIImage) -> some View {
#if os(iOS)
UserProfileImagePicker.SquareImageCropView(
image: image
)
UserProfileImagePicker.SquareImageCropView(viewModel: viewModel, image: image)
#else
AssertionFailureView("not implemented")
#endif
Expand All @@ -32,7 +47,7 @@ final class UserProfileImageCoordinator: NavigationCoordinatable {
@ViewBuilder
func makeStart() -> some View {
#if os(iOS)
UserProfileImagePicker()
UserProfileImagePicker(viewModel: viewModel)
#else
AssertionFailureView("not implemented")
#endif
Expand Down
5 changes: 3 additions & 2 deletions Shared/Services/Notifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ extension Notifications.Key {

// MARK: - User

static var didChangeUserProfileImage: Key<Void> {
Key("didChangeUserProfileImage")
/// - Payload: The ID of the user whose Profile Image changed.
static var didChangeUserProfile: Key<String> {
Key("didChangeUserProfile")
}

static var didAddServerUser: Key<UserDto> {
Expand Down
18 changes: 14 additions & 4 deletions Shared/Strings/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ internal enum L10n {
internal static let absolute = L10n.tr("Localizable", "absolute", fallback: "Absolute")
/// Accent Color
internal static let accentColor = L10n.tr("Localizable", "accentColor", fallback: "Accent Color")
/// Some views may need an app restart to update.
internal static let accentColorDescription = L10n.tr("Localizable", "accentColorDescription", fallback: "Some views may need an app restart to update.")
/// Access
internal static let access = L10n.tr("Localizable", "access", fallback: "Access")
/// Accessibility
Expand Down Expand Up @@ -46,8 +44,8 @@ internal enum L10n {
internal static let additionalSecurityAccessDescription = L10n.tr("Localizable", "additionalSecurityAccessDescription", fallback: "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.")
/// Add Server
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
/// Add Trigger
internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add Trigger")
/// Add trigger
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
Expand Down Expand Up @@ -1046,6 +1044,8 @@ internal enum L10n {
internal static let production = L10n.tr("Localizable", "production", fallback: "Production")
/// Production Locations
internal static let productionLocations = L10n.tr("Localizable", "productionLocations", fallback: "Production Locations")
/// Profile Image
internal static let profileImage = L10n.tr("Localizable", "profileImage", fallback: "Profile Image")
/// Profiles
internal static let profiles = L10n.tr("Localizable", "profiles", fallback: "Profiles")
/// Programs
Expand Down Expand Up @@ -1164,6 +1164,12 @@ internal enum L10n {
internal static let resetAllSettings = L10n.tr("Localizable", "resetAllSettings", fallback: "Reset all settings back to defaults.")
/// Reset App Settings
internal static let resetAppSettings = L10n.tr("Localizable", "resetAppSettings", fallback: "Reset App Settings")
/// Reset Settings
internal static let resetSettings = L10n.tr("Localizable", "resetSettings", fallback: "Reset Settings")
/// Reset Swiftfin user settings
internal static let resetSettingsDescription = L10n.tr("Localizable", "resetSettingsDescription", fallback: "Reset Swiftfin user settings")
/// Are you sure you want to reset all user settings?
internal static let resetSettingsMessage = L10n.tr("Localizable", "resetSettingsMessage", fallback: "Are you sure you want to reset all user settings?")
/// Reset User Settings
internal static let resetUserSettings = L10n.tr("Localizable", "resetUserSettings", fallback: "Reset User Settings")
/// Restart Server
Expand Down Expand Up @@ -1238,6 +1244,8 @@ internal enum L10n {
internal static let selectAll = L10n.tr("Localizable", "selectAll", fallback: "Select All")
/// Select Cast Destination
internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination", fallback: "Select Cast Destination")
/// Select Image
internal static let selectImage = L10n.tr("Localizable", "selectImage", fallback: "Select Image")
/// Series
internal static let series = L10n.tr("Localizable", "series", fallback: "Series")
/// Series Backdrop
Expand Down Expand Up @@ -1566,6 +1574,8 @@ internal enum L10n {
internal static let videoResolutionNotSupported = L10n.tr("Localizable", "videoResolutionNotSupported", fallback: "The video resolution is not supported")
/// Video transcoding
internal static let videoTranscoding = L10n.tr("Localizable", "videoTranscoding", fallback: "Video transcoding")
/// Some views may need an app restart to update.
internal static let viewsMayRequireRestart = L10n.tr("Localizable", "viewsMayRequireRestart", fallback: "Some views may need an app restart to update.")
/// Weekday
internal static let weekday = L10n.tr("Localizable", "weekday", fallback: "Weekday")
/// Weekend
Expand Down
Loading