Skip to content

Commit

Permalink
Merge branch 'jellyfin:main' into iOSSettingsUpdate
Browse files Browse the repository at this point in the history
  • Loading branch information
JPKribs authored Dec 29, 2024
2 parents 23ac711 + 23beb08 commit 131e7e1
Show file tree
Hide file tree
Showing 44 changed files with 753 additions and 363 deletions.
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
1 change: 1 addition & 0 deletions Shared/Components/ImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ struct ImageView: View {
}
}
.pipeline(pipeline)
.onDisappear(.lowerPriority)
} else {
failure()
.eraseToAnyView()
Expand Down
101 changes: 101 additions & 0 deletions Shared/Components/UserProfileImage/UserProfileHeroImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// 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 UserProfileHeroImage: 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.posters,
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) {
UserProfileImage(
userID: user.id,
source: source,
pipeline: userSession?.user.id == user.id ? .Swiftfin.local : .Swiftfin.posters
)
.frame(width: 150, height: 150)

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,
isPresented: $isPresentingOptions,
titleVisibility: .visible
) {
Button(L10n.selectImage, action: onUpdate)

Button(L10n.delete, role: .destructive, action: onDelete)
}
}
}
86 changes: 86 additions & 0 deletions Shared/Components/UserProfileImage/UserProfileImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// 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 UserProfileImage<Placeholder: View>: View {

// MARK: - Inject Logger

@Injected(\.logService)
private var logger

// MARK: - User Variables

private let userID: String?
private let source: ImageSource
private let pipeline: ImagePipeline
private let 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)
}
}
}

// MARK: - Initializer

extension UserProfileImage {

init(
userID: String?,
source: ImageSource,
pipeline: ImagePipeline = .Swiftfin.posters,
@ViewBuilder placeholder: @escaping () -> Placeholder
) {
self.userID = userID
self.source = source
self.pipeline = pipeline
self.placeholder = placeholder()
}
}

extension UserProfileImage where Placeholder == SystemImageContentView {

init(
userID: String?,
source: ImageSource,
pipeline: ImagePipeline = .Swiftfin.posters
) {
self.userID = userID
self.source = source
self.pipeline = pipeline
self.placeholder = SystemImageContentView(systemName: "person.fill", ratio: 0.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
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ extension BaseItemDto {
maxWidth: maxWidth,
maxHeight: maxHeight,
itemID: seriesID ?? "",
force: true
requireTag: false
)
}

Expand All @@ -70,7 +70,7 @@ extension BaseItemDto {
maxWidth: maxWidth,
maxHeight: maxHeight,
itemID: seriesID ?? "",
force: true
requireTag: false
)

return ImageSource(
Expand All @@ -86,16 +86,14 @@ extension BaseItemDto {
maxWidth: CGFloat?,
maxHeight: CGFloat?,
itemID: String,
force: Bool = false
requireTag: Bool = true
) -> URL? {
let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!)
let scaleHeight = maxHeight == nil ? nil : UIScreen.main.scale(maxHeight!)

let tag = getImageTag(for: type)

if tag == nil && !force {
return nil
}
guard tag != nil || !requireTag else { return nil }

// TODO: client passing for widget/shared group views?
guard let client = Container.shared.currentUserSession()?.client else { return nil }
Expand Down
Loading

0 comments on commit 131e7e1

Please sign in to comment.