[iOS & tvOS] Fix Square Overlay Over Profiles (#1466)

* Fix Square over Circle

* cleanup

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-04-06 15:21:21 -06:00 committed by GitHub
parent 0de40a5788
commit 26ec19982e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 100 additions and 133 deletions

View File

@ -6,18 +6,19 @@
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import Factory
import JellyfinAPI
import Nuke
import SwiftUI
struct UserProfileImage<Placeholder: View>: View {
// MARK: - Inject Logger
// MARK: - Environment Variables
@Injected(\.logService)
private var logger
@Environment(\.isEnabled)
private var isEnabled
@Environment(\.isEditing)
private var isEditing
@Environment(\.isSelected)
private var isSelected
// MARK: - User Variables
@ -26,6 +27,17 @@ struct UserProfileImage<Placeholder: View>: View {
private let pipeline: ImagePipeline
private let placeholder: Placeholder
// MARK: - Overlay Opacity
private var overlayOpacity: Double {
/// Dim the Profile Image if Editing & Unselected or if Disabled
if (isEditing && !isSelected) || !isEnabled {
return 0.5
} else {
return 0.0
}
}
// MARK: - Body
var body: some View {
@ -46,9 +58,12 @@ struct UserProfileImage<Placeholder: View>: View {
.failure {
placeholder
}
.posterShadow()
.overlay {
Color.black
.opacity(overlayOpacity)
}
.aspectRatio(1, contentMode: .fill)
.clipShape(Circle())
.clipShape(.circle)
.shadow(radius: 5)
}
}

View File

@ -52,66 +52,51 @@ extension SelectUserView {
return isSelected ? .primary : .secondary
}
// MARK: - User Portrait
private var userPortrait: some View {
UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
),
pipeline: .Swiftfin.local
)
.aspectRatio(1, contentMode: .fill)
.clipShape(.circle)
.overlay {
if isEditing {
ZStack(alignment: .bottom) {
Color.black
.opacity(isSelected ? 0 : 0.5)
if isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 75, height: 75)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
}
}
}
}
}
// MARK: - Body
var body: some View {
VStack {
Button {
action()
} label: {
userPortrait
.hoverEffect(.highlight)
Text(user.username)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(labelForegroundStyle)
.lineLimit(1)
if showServer {
Text(server.name)
.font(.footnote)
.foregroundStyle(.secondary)
Button(action: action) {
UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
),
pipeline: .Swiftfin.local
)
.overlay(alignment: .bottom) {
if isEditing && isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 75, height: 75)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
}
}
.buttonStyle(.borderless)
.buttonBorderShape(.circle)
.contextMenu {
Button(L10n.delete, role: .destructive) {
onDelete()
}
.hoverEffect(.highlight)
Text(user.username)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(labelForegroundStyle)
.lineLimit(1)
if showServer {
Text(server.name)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.buttonStyle(.borderless)
.buttonBorderShape(.circle)
.contextMenu {
if !isEditing {
Button(
L10n.delete,
role: .destructive,
action: onDelete
)
}
}
}

View File

@ -82,12 +82,9 @@ extension ServerUsersView {
maxWidth: 60
)
)
.grayscale(userActive ? 0.0 : 1.0)
if isEditing {
Color.black
.opacity(isSelected ? 0 : 0.5)
}
.environment(\.isEnabled, userActive)
.environment(\.isEditing, isEditing)
.environment(\.isSelected, isSelected)
}
.frame(width: 60, height: 60)
}

View File

@ -17,8 +17,6 @@ extension SelectUserView {
@Default(.accentColor)
private var accentColor
@Environment(\.colorScheme)
private var colorScheme
@Environment(\.isEditing)
private var isEditing
@Environment(\.isSelected)
@ -51,40 +49,24 @@ extension SelectUserView {
}
var body: some View {
Button {
action()
} label: {
VStack(alignment: .center) {
ZStack {
Color.clear
UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
),
pipeline: .Swiftfin.local
)
}
.aspectRatio(1, contentMode: .fill)
.clipShape(.circle)
.overlay {
if isEditing {
ZStack(alignment: .bottomTrailing) {
Color.black
.opacity(isSelected ? 0 : 0.5)
.clipShape(.circle)
if isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40, alignment: .bottomTrailing)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
}
}
Button(action: action) {
VStack {
UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
),
pipeline: .Swiftfin.local
)
.overlay(alignment: .bottomTrailing) {
if isEditing, isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40, alignment: .bottomTrailing)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
}
}
@ -103,8 +85,12 @@ extension SelectUserView {
}
.buttonStyle(.plain)
.contextMenu {
Button(L10n.delete, role: .destructive) {
onDelete()
if !isEditing {
Button(
L10n.delete,
role: .destructive,
action: onDelete
)
}
}
}

View File

@ -69,29 +69,6 @@ extension SelectUserView {
.aspectRatio(1, contentMode: .fill)
}
@ViewBuilder
private var userImage: some View {
ZStack {
Color.clear
UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
),
pipeline: .Swiftfin.local
)
if isEditing {
Color.black
.opacity(isSelected ? 0 : 0.5)
}
}
.aspectRatio(contentMode: .fill)
.clipShape(.circle)
}
@ViewBuilder
private var rowContent: some View {
HStack {
@ -119,9 +96,16 @@ extension SelectUserView {
var body: some View {
ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) {
userImage
.frame(width: 80)
.padding(.vertical, 8)
UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
),
pipeline: .Swiftfin.local
)
.frame(width: 80)
.padding(.vertical, 8)
} content: {
rowContent
}