mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
153
Yattee/Views/Navigation/SidebarChannelIcon.swift
Normal file
153
Yattee/Views/Navigation/SidebarChannelIcon.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// SidebarChannelIcon.swift
|
||||
// Yattee
|
||||
//
|
||||
// Pre-scaled channel icon for TabSection labels.
|
||||
// TabSection labels don't support frame/resizable modifiers,
|
||||
// so we pre-scale the image at the platform image level.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Nuke
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
/// A channel icon that pre-scales the image for use in TabSection labels.
|
||||
/// Standard SwiftUI frame/resizable modifiers don't work in Tab labels.
|
||||
struct SidebarChannelIcon: View {
|
||||
let url: URL?
|
||||
let name: String
|
||||
var authHeader: String?
|
||||
|
||||
private let size: CGFloat = 22
|
||||
|
||||
@State private var platformImage: PlatformImage?
|
||||
@State private var isLoading = false
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let platformImage, let scaledImage = scaledImage(from: platformImage) {
|
||||
scaledImage
|
||||
} else {
|
||||
// Placeholder - use SF Symbol which scales correctly
|
||||
Image(systemName: "person.circle.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadImage()
|
||||
}
|
||||
.onChange(of: url) { _, _ in
|
||||
platformImage = nil
|
||||
loadImage()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func scaledImage(from image: PlatformImage) -> Image? {
|
||||
#if os(macOS)
|
||||
if let scaled = image.scaledCircular(to: NSSize(width: size, height: size)) {
|
||||
Image(nsImage: scaled)
|
||||
}
|
||||
#else
|
||||
if let scaled = image.scaledCircular(to: CGSize(width: size, height: size)) {
|
||||
Image(uiImage: scaled)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func loadImage() {
|
||||
guard let request = AvatarURLBuilder.imageRequest(url: url, authHeader: authHeader), !isLoading else { return }
|
||||
|
||||
// Check memory cache first (synchronous)
|
||||
if let cached = ImagePipeline.shared.cache.cachedImage(for: request)?.image {
|
||||
platformImage = cached
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
|
||||
Task {
|
||||
do {
|
||||
let image = try await ImagePipeline.shared.image(for: request)
|
||||
await MainActor.run {
|
||||
platformImage = image
|
||||
isLoading = false
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Platform Image Scaling
|
||||
|
||||
#if os(macOS)
|
||||
private extension NSImage {
|
||||
func scaledCircular(to targetSize: NSSize) -> NSImage? {
|
||||
let newImage = NSImage(size: targetSize)
|
||||
newImage.lockFocus()
|
||||
|
||||
// Create circular clipping path
|
||||
let path = NSBezierPath(ovalIn: NSRect(origin: .zero, size: targetSize))
|
||||
path.addClip()
|
||||
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
|
||||
draw(
|
||||
in: NSRect(origin: .zero, size: targetSize),
|
||||
from: NSRect(origin: .zero, size: size),
|
||||
operation: .copy,
|
||||
fraction: 1.0
|
||||
)
|
||||
|
||||
newImage.unlockFocus()
|
||||
return newImage
|
||||
}
|
||||
}
|
||||
#else
|
||||
private extension UIImage {
|
||||
func scaledCircular(to targetSize: CGSize) -> UIImage? {
|
||||
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
||||
return renderer.image { context in
|
||||
// Create circular clipping path
|
||||
let rect = CGRect(origin: .zero, size: targetSize)
|
||||
context.cgContext.addEllipse(in: rect)
|
||||
context.cgContext.clip()
|
||||
|
||||
draw(in: rect)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#if !os(tvOS)
|
||||
#Preview {
|
||||
List {
|
||||
Label {
|
||||
Text("Apple")
|
||||
} icon: {
|
||||
SidebarChannelIcon(url: nil, name: "Apple")
|
||||
}
|
||||
|
||||
Label {
|
||||
Text("Test Channel")
|
||||
} icon: {
|
||||
SidebarChannelIcon(
|
||||
url: URL(string: "https://example.com/avatar.jpg"),
|
||||
name: "Test"
|
||||
)
|
||||
}
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user