[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:
parent
0de40a5788
commit
26ec19982e
|
@ -6,18 +6,19 @@
|
||||||
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
import Defaults
|
|
||||||
import Factory
|
|
||||||
import JellyfinAPI
|
|
||||||
import Nuke
|
import Nuke
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UserProfileImage<Placeholder: View>: View {
|
struct UserProfileImage<Placeholder: View>: View {
|
||||||
|
|
||||||
// MARK: - Inject Logger
|
// MARK: - Environment Variables
|
||||||
|
|
||||||
@Injected(\.logService)
|
@Environment(\.isEnabled)
|
||||||
private var logger
|
private var isEnabled
|
||||||
|
@Environment(\.isEditing)
|
||||||
|
private var isEditing
|
||||||
|
@Environment(\.isSelected)
|
||||||
|
private var isSelected
|
||||||
|
|
||||||
// MARK: - User Variables
|
// MARK: - User Variables
|
||||||
|
|
||||||
|
@ -26,6 +27,17 @@ struct UserProfileImage<Placeholder: View>: View {
|
||||||
private let pipeline: ImagePipeline
|
private let pipeline: ImagePipeline
|
||||||
private let placeholder: Placeholder
|
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
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -46,9 +58,12 @@ struct UserProfileImage<Placeholder: View>: View {
|
||||||
.failure {
|
.failure {
|
||||||
placeholder
|
placeholder
|
||||||
}
|
}
|
||||||
.posterShadow()
|
.overlay {
|
||||||
|
Color.black
|
||||||
|
.opacity(overlayOpacity)
|
||||||
|
}
|
||||||
.aspectRatio(1, contentMode: .fill)
|
.aspectRatio(1, contentMode: .fill)
|
||||||
.clipShape(Circle())
|
.clipShape(.circle)
|
||||||
.shadow(radius: 5)
|
.shadow(radius: 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,66 +52,51 @@ extension SelectUserView {
|
||||||
return isSelected ? .primary : .secondary
|
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
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
Button(action: action) {
|
||||||
Button {
|
UserProfileImage(
|
||||||
action()
|
userID: user.id,
|
||||||
} label: {
|
source: user.profileImageSource(
|
||||||
userPortrait
|
client: server.client,
|
||||||
.hoverEffect(.highlight)
|
maxWidth: 120
|
||||||
|
),
|
||||||
Text(user.username)
|
pipeline: .Swiftfin.local
|
||||||
.font(.title3)
|
)
|
||||||
.fontWeight(.semibold)
|
.overlay(alignment: .bottom) {
|
||||||
.foregroundStyle(labelForegroundStyle)
|
if isEditing && isSelected {
|
||||||
.lineLimit(1)
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.resizable()
|
||||||
if showServer {
|
.aspectRatio(contentMode: .fit)
|
||||||
Text(server.name)
|
.frame(width: 75, height: 75)
|
||||||
.font(.footnote)
|
.symbolRenderingMode(.palette)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(accentColor.overlayColor, accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.hoverEffect(.highlight)
|
||||||
.buttonBorderShape(.circle)
|
|
||||||
.contextMenu {
|
Text(user.username)
|
||||||
Button(L10n.delete, role: .destructive) {
|
.font(.title3)
|
||||||
onDelete()
|
.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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,12 +82,9 @@ extension ServerUsersView {
|
||||||
maxWidth: 60
|
maxWidth: 60
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.grayscale(userActive ? 0.0 : 1.0)
|
.environment(\.isEnabled, userActive)
|
||||||
|
.environment(\.isEditing, isEditing)
|
||||||
if isEditing {
|
.environment(\.isSelected, isSelected)
|
||||||
Color.black
|
|
||||||
.opacity(isSelected ? 0 : 0.5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.frame(width: 60, height: 60)
|
.frame(width: 60, height: 60)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,6 @@ extension SelectUserView {
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
private var accentColor
|
private var accentColor
|
||||||
|
|
||||||
@Environment(\.colorScheme)
|
|
||||||
private var colorScheme
|
|
||||||
@Environment(\.isEditing)
|
@Environment(\.isEditing)
|
||||||
private var isEditing
|
private var isEditing
|
||||||
@Environment(\.isSelected)
|
@Environment(\.isSelected)
|
||||||
|
@ -51,40 +49,24 @@ extension SelectUserView {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button(action: action) {
|
||||||
action()
|
VStack {
|
||||||
} label: {
|
UserProfileImage(
|
||||||
VStack(alignment: .center) {
|
userID: user.id,
|
||||||
ZStack {
|
source: user.profileImageSource(
|
||||||
Color.clear
|
client: server.client,
|
||||||
|
maxWidth: 120
|
||||||
UserProfileImage(
|
),
|
||||||
userID: user.id,
|
pipeline: .Swiftfin.local
|
||||||
source: user.profileImageSource(
|
)
|
||||||
client: server.client,
|
.overlay(alignment: .bottomTrailing) {
|
||||||
maxWidth: 120
|
if isEditing, isSelected {
|
||||||
),
|
Image(systemName: "checkmark.circle.fill")
|
||||||
pipeline: .Swiftfin.local
|
.resizable()
|
||||||
)
|
.aspectRatio(contentMode: .fit)
|
||||||
}
|
.frame(width: 40, height: 40, alignment: .bottomTrailing)
|
||||||
.aspectRatio(1, contentMode: .fill)
|
.symbolRenderingMode(.palette)
|
||||||
.clipShape(.circle)
|
.foregroundStyle(accentColor.overlayColor, accentColor)
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,8 +85,12 @@ extension SelectUserView {
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button(L10n.delete, role: .destructive) {
|
if !isEditing {
|
||||||
onDelete()
|
Button(
|
||||||
|
L10n.delete,
|
||||||
|
role: .destructive,
|
||||||
|
action: onDelete
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,29 +69,6 @@ extension SelectUserView {
|
||||||
.aspectRatio(1, contentMode: .fill)
|
.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
|
@ViewBuilder
|
||||||
private var rowContent: some View {
|
private var rowContent: some View {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -119,9 +96,16 @@ extension SelectUserView {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) {
|
ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) {
|
||||||
userImage
|
UserProfileImage(
|
||||||
.frame(width: 80)
|
userID: user.id,
|
||||||
.padding(.vertical, 8)
|
source: user.profileImageSource(
|
||||||
|
client: server.client,
|
||||||
|
maxWidth: 120
|
||||||
|
),
|
||||||
|
pipeline: .Swiftfin.local
|
||||||
|
)
|
||||||
|
.frame(width: 80)
|
||||||
|
.padding(.vertical, 8)
|
||||||
} content: {
|
} content: {
|
||||||
rowContent
|
rowContent
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue