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: deleteMessage)64 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)65 .onAppear {66 if appState.saveToFolderEnabled {67 localManager.scanFolder(path: appState.saveFolderPath)68 }69 }70 }7172 func scan() { localManager.scanFolder(path: appState.saveFolderPath) }73 func set(_ wallpaper: LocalWallpaper) async { await appState.setLocalWallpaper(wallpaper) }74 func requestDelete(_ wallpaper: LocalWallpaper) {75 wallpaperToDelete = wallpaper76 showingDeleteConfirmation = true77 }78 func delete(_ wallpaper: LocalWallpaper) { localManager.deleteWallpaper(wallpaper) }79 func cancelDelete() { wallpaperToDelete = nil }80 func deleteMessage(_ wallpaper: LocalWallpaper) -> Text {81 Text("This will permanently delete '\(wallpaper.filename)' from your save folder.")82 }83 @ViewBuilder func deleteActions(_ wallpaper: LocalWallpaper) -> some View {84 Button("Cancel", role: .cancel, action: cancelDelete)85 Button("Delete", role: .destructive) { delete(wallpaper) }86 }87}88899091struct LocalWallpaperGrid: View {92 let wallpapers: [LocalWallpaper]93 let onSetWallpaper: (LocalWallpaper) -> Void94 let onDeleteWallpaper: (LocalWallpaper) -> Void9596 var body: some View {97 ScrollView {98 LazyVGrid(columns: [GridItem(.adaptive(minimum: 140), spacing: 12)], spacing: 12) {99 ForEach(wallpapers) { wallpaper in100 LocalWallpaperItem(101 wallpaper: wallpaper,102 onSet: { onSetWallpaper(wallpaper) },103 onDelete: { onDeleteWallpaper(wallpaper) }104 )105 }106 }107 .padding(12)108 }109 }110}111112113114struct LocalWallpaperItem: View {115 let wallpaper: LocalWallpaper116 let onSet: () -> Void117 let onDelete: () -> Void118119 @State private var isHovering = false120 @State private var image: NSImage?121 @State private var isLoadingImage = true122123 init(124 wallpaper: LocalWallpaper, onSet: @escaping () -> Void, onDelete: @escaping () -> Void,125 initialImage: NSImage? = nil, isLoadingImage: Bool = true126 ) {127 self.wallpaper = wallpaper128 self.onSet = onSet129 self.onDelete = onDelete130 _image = State(initialValue: initialImage)131 _isLoadingImage = State(initialValue: isLoadingImage)132 }133134 var body: some View {135 Button(action: onSet) {136 ZStack {137 if let image = image {138 Image(nsImage: image)139 .resizable()140 .aspectRatio(contentMode: .fill)141 .clipped()142 } else if isLoadingImage {143 Rectangle()144 .fill(Color.gray.opacity(0.2))145 .overlay(ProgressView())146 } else {147 Rectangle()148 .fill(Color.gray.opacity(0.1))149 .overlay(150 VStack {151 Image(systemName: "photo")152 .foregroundColor(.gray)153 Text(wallpaper.filename)154 .font(.caption2)155 .foregroundColor(.secondary)156 .lineLimit(1)157 }158 )159 }160 }161 .frame(maxWidth: .infinity, minHeight: 100, maxHeight: 100)162 .cornerRadius(8)163 .overlay(164 RoundedRectangle(cornerRadius: 8)165 .stroke(isHovering ? Color.accentColor : Color.clear, lineWidth: 2)166 )167 .shadow(radius: isHovering ? 4 : 0)168 .overlay(169 170 HStack {171 172 Button(action: onDelete) {173 Image(systemName: "trash")174 .foregroundColor(.white)175 .shadow(radius: 2)176 .font(.system(size: 14, weight: .bold))177 .padding(6)178 .background(Color.red.opacity(0.7))179 .clipShape(Circle())180 }181 .buttonStyle(.plain)182 .padding(6)183184 Spacer()185186 187 Text(wallpaper.resolution)188 .font(.caption2)189 .padding(.horizontal, 6)190 .padding(.vertical, 2)191 .background(Color.black.opacity(0.6))192 .foregroundColor(.white)193 .cornerRadius(4)194 .padding(6)195 }196 .opacity(isHovering ? 1 : 0)197 .animation(.easeInOut(duration: 0.15), value: isHovering),198 alignment: .top199 )200 .scaleEffect(isHovering ? 1.02 : 1.0)201 .animation(.spring(response: 0.3), value: isHovering)202 }203 .buttonStyle(.plain)204 .onHover { isHovering = $0 }205 .help("\(wallpaper.resolution) • \(formatFileSize(wallpaper.fileSize))")206 .onAppear {207 loadImage()208 }209 }210211 212 213 214 215 private static let thumbnailMaxPixel: Int = 320216217 private func loadImage() {218 isLoadingImage = true219 let url = self.wallpaper.url220 let maxPixel = Self.thumbnailMaxPixel221222 DispatchQueue.global(qos: .userInitiated).async {223 let loadedImage = LocalWallpaperItem.makeThumbnail(url: url, maxPixel: maxPixel)224 DispatchQueue.main.async {225 self.image = loadedImage226 self.isLoadingImage = false227 }228 }229 }230231 232 233 234 235 static func makeThumbnail(url: URL, maxPixel: Int) -> NSImage? {236 guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil }237 let options: [CFString: Any] = [238 kCGImageSourceCreateThumbnailFromImageAlways: true,239 kCGImageSourceCreateThumbnailWithTransform: true,240 kCGImageSourceShouldCacheImmediately: true,241 kCGImageSourceThumbnailMaxPixelSize: maxPixel,242 ]243 guard244 let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)245 else { return nil }246 let size = NSSize(width: cgImage.width, height: cgImage.height)247 return NSImage(cgImage: cgImage, size: size)248 }249250 private func formatFileSize(_ bytes: Int64) -> String {251 let formatter = ByteCountFormatter()252 formatter.allowedUnits = [.useKB, .useMB, .useGB]253 formatter.countStyle = .file254 return formatter.string(fromByteCount: bytes)255 }256}257258259260struct LoadingState: View {261 var body: some View {262 VStack(spacing: 16) {263 ProgressView()264 Text("Loading wallpapers...")265 .foregroundColor(.secondary)266 }267 .frame(maxWidth: .infinity, maxHeight: .infinity)268 }269}