1import SwiftUI23struct LocalView: View {4 @ObservedObject var appState: AppState5 @ObservedObject var localManager: LocalWallpapersManager6 @State private var selectedWallpaper: LocalWallpaper?7 @State private var showingDeleteConfirmation = false8 @State private var wallpaperToDelete: LocalWallpaper?910 init(appState: AppState, localManager: LocalWallpapersManager? = nil) {11 self.appState = appState12 self.localManager = localManager ?? .shared13 }1415 var body: some View {16 VStack(spacing: 0) {17 18 HStack {19 Text("Local Wallpapers")20 .font(.headline)2122 Spacer()2324 if !localManager.wallpapers.isEmpty {25 Text("\(localManager.wallpapers.count) items")26 .font(.caption)27 .foregroundColor(.secondary)28 }2930 Button(action: scan) {31 Image(systemName: "arrow.clockwise")32 }33 .buttonStyle(.plain)34 .disabled(localManager.isLoading)35 }36 .padding()3738 Divider()3940 41 if localManager.isLoading && localManager.wallpapers.isEmpty {42 LoadingState()43 } else if !appState.saveToFolderEnabled {44 EmptyState(45 icon: "folder.badge.questionmark",46 text: "Enable 'Save wallpapers locally' in Settings to view local wallpapers"47 )48 } else if localManager.wallpapers.isEmpty {49 EmptyState(50 icon: "photo.on.rectangle.angled",51 text: "No wallpapers found in your save folder"52 )53 } else {54 LocalWallpaperGrid(55 wallpapers: localManager.wallpapers,56 onSetWallpaper: { wallpaper in Task { await set(wallpaper) } },57 onDeleteWallpaper: requestDelete58 )59 }60 }61 .alert(62 "Delete Wallpaper?", isPresented: $showingDeleteConfirmation, presenting: wallpaperToDelete,63 actions: deleteActions, message: deleteMessage64 )65 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)66 .onAppear {67 if appState.saveToFolderEnabled {68 localManager.scanFolder(path: appState.saveFolderPath)69 }70 }71 }7273 func scan() { localManager.scanFolder(path: appState.saveFolderPath) }74 func set(_ wallpaper: LocalWallpaper) async { await appState.setLocalWallpaper(wallpaper) }75 func requestDelete(_ wallpaper: LocalWallpaper) {76 wallpaperToDelete = wallpaper77 showingDeleteConfirmation = true78 }79 func delete(_ wallpaper: LocalWallpaper) { localManager.deleteWallpaper(wallpaper) }80 func cancelDelete() { wallpaperToDelete = nil }81 func deleteMessage(_ wallpaper: LocalWallpaper) -> Text {82 Text("This will permanently delete '\(wallpaper.filename)' from your save folder.")83 }84 @ViewBuilder func deleteActions(_ wallpaper: LocalWallpaper) -> some View {85 Button("Cancel", role: .cancel, action: cancelDelete)86 Button("Delete", role: .destructive) { delete(wallpaper) }87 }88}89909192struct LocalWallpaperGrid: View {93 let wallpapers: [LocalWallpaper]94 let onSetWallpaper: (LocalWallpaper) -> Void95 let onDeleteWallpaper: (LocalWallpaper) -> Void9697 var body: some View {98 ScrollView {99 LazyVGrid(columns: [GridItem(.adaptive(minimum: 140), spacing: 12)], spacing: 12) {100 ForEach(wallpapers) { wallpaper in101 LocalWallpaperItem(102 wallpaper: wallpaper,103 onSet: { onSetWallpaper(wallpaper) },104 onDelete: { onDeleteWallpaper(wallpaper) }105 )106 }107 }108 .padding(12)109 }110 }111}112113114115struct LocalWallpaperItem: View {116 let wallpaper: LocalWallpaper117 let onSet: () -> Void118 let onDelete: () -> Void119120 @State private var isHovering = false121 @State private var image: NSImage?122 @State private var isLoadingImage = true123124 init(125 wallpaper: LocalWallpaper, onSet: @escaping () -> Void, onDelete: @escaping () -> Void,126 initialImage: NSImage? = nil, isLoadingImage: Bool = true127 ) {128 self.wallpaper = wallpaper129 self.onSet = onSet130 self.onDelete = onDelete131 _image = State(initialValue: initialImage)132 _isLoadingImage = State(initialValue: isLoadingImage)133 }134135 var body: some View {136 Button(action: onSet) {137 ZStack {138 if let image = image {139 Image(nsImage: image)140 .resizable()141 .aspectRatio(contentMode: .fill)142 .clipped()143 } else if isLoadingImage {144 Rectangle()145 .fill(Color.gray.opacity(0.2))146 .overlay(ProgressView())147 } else {148 Rectangle()149 .fill(Color.gray.opacity(0.1))150 .overlay(151 VStack {152 Image(systemName: "photo")153 .foregroundColor(.gray)154 Text(wallpaper.filename)155 .font(.caption2)156 .foregroundColor(.secondary)157 .lineLimit(1)158 }159 )160 }161 }162 .frame(maxWidth: .infinity, minHeight: 100, maxHeight: 100)163 .cornerRadius(8)164 .overlay(165 RoundedRectangle(cornerRadius: 8)166 .stroke(isHovering ? Color.accentColor : Color.clear, lineWidth: 2)167 )168 .shadow(radius: isHovering ? 4 : 0)169 .overlay(170 171 HStack {172 173 Button(action: onDelete) {174 Image(systemName: "trash")175 .foregroundColor(.white)176 .shadow(radius: 2)177 .font(.system(size: 14, weight: .bold))178 .padding(6)179 .background(Color.red.opacity(0.7))180 .clipShape(Circle())181 }182 .buttonStyle(.plain)183 .padding(6)184185 Spacer()186187 188 Text(wallpaper.resolution)189 .font(.caption2)190 .padding(.horizontal, 6)191 .padding(.vertical, 2)192 .background(Color.black.opacity(0.6))193 .foregroundColor(.white)194 .cornerRadius(4)195 .padding(6)196 }197 .opacity(isHovering ? 1 : 0)198 .animation(.easeInOut(duration: 0.15), value: isHovering),199 alignment: .top200 )201 .scaleEffect(isHovering ? 1.02 : 1.0)202 .animation(.spring(response: 0.3), value: isHovering)203 }204 .buttonStyle(.plain)205 .onHover { isHovering = $0 }206 .help("\(wallpaper.resolution) • \(formatFileSize(wallpaper.fileSize))")207 .onAppear {208 loadImage()209 }210 }211212 213 214 215 216 private static let thumbnailMaxPixel: Int = 320217218 private func loadImage() {219 isLoadingImage = true220 let url = self.wallpaper.url221 let maxPixel = Self.thumbnailMaxPixel222223 DispatchQueue.global(qos: .userInitiated).async {224 let loadedImage = LocalWallpaperItem.makeThumbnail(url: url, maxPixel: maxPixel)225 DispatchQueue.main.async {226 self.image = loadedImage227 self.isLoadingImage = false228 }229 }230 }231232 233 234 235 236 static func makeThumbnail(url: URL, maxPixel: Int) -> NSImage? {237 guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil }238 let options: [CFString: Any] = [239 kCGImageSourceCreateThumbnailFromImageAlways: true,240 kCGImageSourceCreateThumbnailWithTransform: true,241 kCGImageSourceShouldCacheImmediately: true,242 kCGImageSourceThumbnailMaxPixelSize: maxPixel,243 ]244 guard245 let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)246 else { return nil }247 let size = NSSize(width: cgImage.width, height: cgImage.height)248 return NSImage(cgImage: cgImage, size: size)249 }250251 private func formatFileSize(_ bytes: Int64) -> String {252 let formatter = ByteCountFormatter()253 formatter.allowedUnits = [.useKB, .useMB, .useGB]254 formatter.countStyle = .file255 return formatter.string(fromByteCount: bytes)256 }257}258259260261struct LoadingState: View {262 var body: some View {263 VStack(spacing: 16) {264 ProgressView()265 Text("Loading wallpapers...")266 .foregroundColor(.secondary)267 }268 .frame(maxWidth: .infinity, maxHeight: .infinity)269 }270}