1import SwiftUI23struct LocalView: View {4 @ObservedObject var appState: AppState5 @ObservedObject private var localManager = LocalWallpapersManager.shared6 @State private var selectedWallpaper: LocalWallpaper?7 @State private var showingDeleteConfirmation = false8 @State private var wallpaperToDelete: LocalWallpaper?9 10 var body: some View {11 VStack(spacing: 0) {12 13 HStack {14 Text("Local Wallpapers")15 .font(.headline)16 17 Spacer()18 19 if !localManager.wallpapers.isEmpty {20 Text("\(localManager.wallpapers.count) items")21 .font(.caption)22 .foregroundColor(.secondary)23 }24 25 Button {26 localManager.scanFolder(path: appState.saveFolderPath)27 } label: {28 Image(systemName: "arrow.clockwise")29 }30 .buttonStyle(.plain)31 .disabled(localManager.isLoading)32 }33 .padding()34 35 Divider()36 37 38 if localManager.isLoading && localManager.wallpapers.isEmpty {39 LoadingState()40 } else if !appState.saveToFolderEnabled {41 EmptyState(42 icon: "folder.badge.questionmark",43 text: "Enable 'Save wallpapers locally' in Settings to view local wallpapers"44 )45 } else if localManager.wallpapers.isEmpty {46 EmptyState(47 icon: "photo.on.rectangle.angled",48 text: "No wallpapers found in your save folder"49 )50 } else {51 LocalWallpaperGrid(52 wallpapers: localManager.wallpapers,53 onSetWallpaper: { wallpaper in54 Task {55 await appState.setLocalWallpaper(wallpaper)56 }57 },58 onDeleteWallpaper: { wallpaper in59 wallpaperToDelete = wallpaper60 showingDeleteConfirmation = true61 }62 )63 }64 }65 .alert("Delete Wallpaper?", isPresented: $showingDeleteConfirmation, presenting: wallpaperToDelete) { wallpaper in66 Button("Cancel", role: .cancel) {}67 Button("Delete", role: .destructive) {68 localManager.deleteWallpaper(wallpaper)69 }70 } message: { wallpaper in71 Text("This will permanently delete '\(wallpaper.filename)' from your save folder.")72 }73 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)74 .onAppear {75 if appState.saveToFolderEnabled {76 localManager.scanFolder(path: appState.saveFolderPath)77 }78 }79 }80}81828384struct LocalWallpaperGrid: View {85 let wallpapers: [LocalWallpaper]86 let onSetWallpaper: (LocalWallpaper) -> Void87 let onDeleteWallpaper: (LocalWallpaper) -> Void88 89 var body: some View {90 ScrollView {91 LazyVGrid(columns: [GridItem(.adaptive(minimum: 140), spacing: 12)], spacing: 12) {92 ForEach(wallpapers) { wallpaper in93 LocalWallpaperItem(94 wallpaper: wallpaper,95 onSet: { onSetWallpaper(wallpaper) },96 onDelete: { onDeleteWallpaper(wallpaper) }97 )98 }99 }100 .padding(12)101 }102 }103}104105106107struct LocalWallpaperItem: View {108 let wallpaper: LocalWallpaper109 let onSet: () -> Void110 let onDelete: () -> Void111 112 @State private var isHovering = false113 @State private var image: NSImage?114 @State private var isLoadingImage = true115 116 var body: some View {117 Button(action: onSet) {118 ZStack {119 if let image = image {120 Image(nsImage: image)121 .resizable()122 .aspectRatio(contentMode: .fill)123 .clipped()124 } else if isLoadingImage {125 Rectangle()126 .fill(Color.gray.opacity(0.2))127 .overlay(ProgressView())128 } else {129 Rectangle()130 .fill(Color.gray.opacity(0.1))131 .overlay(132 VStack {133 Image(systemName: "photo")134 .foregroundColor(.gray)135 Text(wallpaper.filename)136 .font(.caption2)137 .foregroundColor(.secondary)138 .lineLimit(1)139 }140 )141 }142 }143 .frame(maxWidth: .infinity, minHeight: 100, maxHeight: 100)144 .cornerRadius(8)145 .overlay(146 RoundedRectangle(cornerRadius: 8)147 .stroke(isHovering ? Color.accentColor : Color.clear, lineWidth: 2)148 )149 .shadow(radius: isHovering ? 4 : 0)150 .overlay(151 152 HStack {153 154 Button {155 onDelete()156 } label: {157 Image(systemName: "trash")158 .foregroundColor(.white)159 .shadow(radius: 2)160 .font(.system(size: 14, weight: .bold))161 .padding(6)162 .background(Color.red.opacity(0.7))163 .clipShape(Circle())164 }165 .buttonStyle(.plain)166 .padding(6)167 168 Spacer()169 170 171 Text(wallpaper.resolution)172 .font(.caption2)173 .padding(.horizontal, 6)174 .padding(.vertical, 2)175 .background(Color.black.opacity(0.6))176 .foregroundColor(.white)177 .cornerRadius(4)178 .padding(6)179 }180 .opacity(isHovering ? 1 : 0)181 .animation(.easeInOut(duration: 0.15), value: isHovering),182 alignment: .top183 )184 .scaleEffect(isHovering ? 1.02 : 1.0)185 .animation(.spring(response: 0.3), value: isHovering)186 }187 .buttonStyle(.plain)188 .onHover { isHovering = $0 }189 .help("\(wallpaper.resolution) • \(formatFileSize(wallpaper.fileSize))")190 .onAppear {191 loadImage()192 }193 }194 195 private func loadImage() {196 isLoadingImage = true197 198 DispatchQueue.global(qos: .userInitiated).async {199 let loadedImage = NSImage(contentsOf: self.wallpaper.url)200 DispatchQueue.main.async {201 self.image = loadedImage202 self.isLoadingImage = false203 }204 }205 }206 207 private func formatFileSize(_ bytes: Int64) -> String {208 let formatter = ByteCountFormatter()209 formatter.allowedUnits = [.useKB, .useMB, .useGB]210 formatter.countStyle = .file211 return formatter.string(fromByteCount: bytes)212 }213}214215216217struct LoadingState: View {218 var body: some View {219 VStack(spacing: 16) {220 ProgressView()221 Text("Loading wallpapers...")222 .foregroundColor(.secondary)223 }224 .frame(maxWidth: .infinity, maxHeight: .infinity)225 }226}