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:
152
Yattee/Views/Navigation/SidebarPlaylistIcon.swift
Normal file
152
Yattee/Views/Navigation/SidebarPlaylistIcon.swift
Normal file
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// SidebarPlaylistIcon.swift
|
||||
// Yattee
|
||||
//
|
||||
// Pre-scaled playlist thumbnail 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 playlist thumbnail that pre-scales the image for use in TabSection labels.
|
||||
/// Standard SwiftUI frame/resizable modifiers don't work in Tab labels.
|
||||
struct SidebarPlaylistIcon: View {
|
||||
let url: URL?
|
||||
|
||||
// Target size: ~26x15 for 16:9 aspect ratio that fits sidebar row height
|
||||
private let targetWidth: CGFloat = 26
|
||||
private let targetHeight: CGFloat = 15
|
||||
private let cornerRadius: CGFloat = 3
|
||||
|
||||
@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 {
|
||||
// Fallback - use SF Symbol which scales correctly
|
||||
Image(systemName: "list.bullet.rectangle")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadImage()
|
||||
}
|
||||
.onChange(of: url) { _, _ in
|
||||
platformImage = nil
|
||||
loadImage()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func scaledImage(from image: PlatformImage) -> Image? {
|
||||
#if os(macOS)
|
||||
if let scaled = image.scaledRounded(to: NSSize(width: targetWidth, height: targetHeight), cornerRadius: cornerRadius) {
|
||||
Image(nsImage: scaled)
|
||||
}
|
||||
#else
|
||||
if let scaled = image.scaledRounded(to: CGSize(width: targetWidth, height: targetHeight), cornerRadius: cornerRadius) {
|
||||
Image(uiImage: scaled)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func loadImage() {
|
||||
guard let url, !isLoading else { return }
|
||||
|
||||
// Check memory cache first (synchronous)
|
||||
if let cached = ImagePipeline.shared.cache.cachedImage(for: ImageRequest(url: url))?.image {
|
||||
platformImage = cached
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
|
||||
Task {
|
||||
do {
|
||||
let image = try await ImagePipeline.shared.image(for: url)
|
||||
await MainActor.run {
|
||||
platformImage = image
|
||||
isLoading = false
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Platform Image Scaling
|
||||
|
||||
#if os(macOS)
|
||||
private extension NSImage {
|
||||
func scaledRounded(to targetSize: NSSize, cornerRadius: CGFloat) -> NSImage? {
|
||||
let newImage = NSImage(size: targetSize)
|
||||
newImage.lockFocus()
|
||||
|
||||
// Create rounded rect clipping path
|
||||
let path = NSBezierPath(roundedRect: NSRect(origin: .zero, size: targetSize), xRadius: cornerRadius, yRadius: cornerRadius)
|
||||
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 scaledRounded(to targetSize: CGSize, cornerRadius: CGFloat) -> UIImage? {
|
||||
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
||||
return renderer.image { context in
|
||||
// Create rounded rect clipping path
|
||||
let rect = CGRect(origin: .zero, size: targetSize)
|
||||
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
|
||||
path.addClip()
|
||||
|
||||
draw(in: rect)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#if !os(tvOS)
|
||||
#Preview {
|
||||
List {
|
||||
Label {
|
||||
Text("My Playlist")
|
||||
} icon: {
|
||||
SidebarPlaylistIcon(url: nil)
|
||||
}
|
||||
|
||||
Label {
|
||||
Text("With Thumbnail")
|
||||
} icon: {
|
||||
SidebarPlaylistIcon(
|
||||
url: URL(string: "https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg")
|
||||
)
|
||||
}
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user