1import Foundation2import Combine3import AppKit45enum AppTab {6 case explore7 case local8 case favorites9 case history10 case settings11}12131415enum FeedMode: String, CaseIterable, Identifiable {16 case latest17 case toplist18 case random1920 var id: String { rawValue }2122 var sorting: WallhavenSorting {23 switch self {24 case .latest: return .dateAdded25 case .toplist: return .toplist26 case .random: return .random27 }28 }2930 var displayName: String {31 switch self {32 case .latest: return "Latest"33 case .toplist: return "Toplist"34 case .random: return "Random"35 }36 }3738 var systemImage: String {39 switch self {40 case .latest: return "clock"41 case .toplist: return "diamond"42 case .random: return "arrow.triangle.2.circlepath"43 }44 }45}4647@MainActor48class AppState: ObservableObject {49 50 @Published var activeTab: AppTab = .explore5152 53 @Published var wallpapers: [Wallpaper] = []54 @Published var favoritesManager = FavoritesManager.shared55 @Published var historyManager = HistoryManager.shared5657 58 @Published var feedMode: FeedMode = .latest59 @Published var searchText: String = ""60 @Published var selectedResolution: String = ""61 @Published var selectedRatio: String = ""62 @Published var isLoading: Bool = false63 @Published var errorMessage: String?64 65 66 @Published var canLoadMore = true67 private var currentPage = 168 private var isFetchingMore = false69 70 71 @Published var saveToFolderEnabled: Bool {72 didSet { UserDefaults.standard.set(saveToFolderEnabled, forKey: "saveToFolderEnabled") }73 }74 @Published var saveFolderPath: String {75 didSet { UserDefaults.standard.set(saveFolderPath, forKey: "saveFolderPath") }76 }77 @Published var launchAtLogin: Bool = AutostartManager.shared.isEnabled {78 didSet { AutostartManager.shared.isEnabled = launchAtLogin }79 }80 81 82 @Published var categories: (general: Bool, anime: Bool, people: Bool) = (true, true, true)83 @Published var purity: (sfw: Bool, sketchy: Bool) = (true, false)84 85 let availableResolutions = ["Any", "1920x1080", "2560x1440", "3840x2160", "5120x2880"]86 let availableRatios = ["Any", "16x9", "16x10", "21x9", "32x9", "4x3"]87 88 private let api = WallhavenAPI()89 private var cancellables = Set<AnyCancellable>()90 91 init() {92 93 self.saveToFolderEnabled = UserDefaults.standard.bool(forKey: "saveToFolderEnabled")94 if let path = UserDefaults.standard.string(forKey: "saveFolderPath") {95 self.saveFolderPath = path96 } else {97 let pictures = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first!98 self.saveFolderPath = pictures.appendingPathComponent("Haven").path99 }100 101 102 $searchText103 .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)104 .sink { [weak self] _ in105 Task { [weak self] in106 await self?.fetchWallpapers()107 }108 }109 .store(in: &cancellables)110 111 112 $categories113 .map { _ in () }114 .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)115 .sink { [weak self] _ in Task { await self?.fetchWallpapers() } }116 .store(in: &cancellables)117 118 $purity119 .map { _ in () }120 .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)121 .sink { [weak self] _ in Task { await self?.fetchWallpapers() } }122 .store(in: &cancellables)123124 $feedMode125 .dropFirst()126 .sink { [weak self] _ in127 Task { [weak self] in await self?.fetchWallpapers(reset: true) }128 }129 .store(in: &cancellables)130131 updateLocalWallpapersMonitoring()132 133 $saveToFolderEnabled134 .dropFirst()135 .sink { [weak self] _ in136 self?.updateLocalWallpapersMonitoring()137 }138 .store(in: &cancellables)139 140 $saveFolderPath141 .dropFirst()142 .sink { [weak self] _ in143 self?.updateLocalWallpapersMonitoring()144 }145 .store(in: &cancellables)146 }147 148 private func updateLocalWallpapersMonitoring() {149 if saveToFolderEnabled {150 LocalWallpapersManager.shared.startMonitoring(folderPath: saveFolderPath)151 } else {152 LocalWallpapersManager.shared.stopMonitoring()153 }154 }155 156 func fetchWallpapers(reset: Bool = true, refreshing: Bool = false) async {157 if reset {158 if !refreshing {159 isLoading = true160 wallpapers = []161 }162 errorMessage = nil163 currentPage = 1164 canLoadMore = true165 } else {166 guard !isFetchingMore && canLoadMore else { return }167 isFetchingMore = true168 }169 170 do {171 let resolutions = selectedResolution == "Any" || selectedResolution.isEmpty ? [] : [selectedResolution]172 let ratios = selectedRatio == "Any" || selectedRatio.isEmpty ? [] : [selectedRatio]173 174 175 let catStr = "\(categories.general ? "1" : "0")\(categories.anime ? "1" : "0")\(categories.people ? "1" : "0")"176 177 let purityStr = "\(purity.sfw ? "1" : "0")\(purity.sketchy ? "1" : "0")0"178 179 let (newWallpapers, meta) = try await api.searchWallpapers(180 query: searchText,181 categories: catStr,182 purity: purityStr,183 sorting: feedMode.sorting,184 resolutions: resolutions,185 ratios: ratios,186 limit: 24,187 page: currentPage188 )189 190 if reset {191 self.wallpapers = newWallpapers192 } else {193 self.wallpapers.append(contentsOf: newWallpapers)194 }195 196 if let meta = meta {197 self.canLoadMore = meta.current_page < meta.last_page198 self.currentPage = meta.current_page + 1199 } else {200 self.canLoadMore = !newWallpapers.isEmpty201 if !newWallpapers.isEmpty { self.currentPage += 1 }202 }203 204 } catch {205 if reset {206 self.errorMessage = error.localizedDescription207 }208 }209 210 if reset {211 isLoading = false212 } else {213 isFetchingMore = false214 }215 }216 217 func loadMore() async {218 await fetchWallpapers(reset: false)219 }220 221 func setWallpaper(_ wallpaper: Wallpaper, on screen: NSScreen? = nil) async {222 do {223 let localURL = try await WallpaperManager.shared.download(from: URL(string: wallpaper.path)!)224 225 try await MainActor.run {226 try WallpaperManager.shared.setDesktopImage(from: localURL, screen: screen)227 }228 229 230 historyManager.addToHistory(wallpaper)231 232 if saveToFolderEnabled {233 let destFolder = URL(fileURLWithPath: saveFolderPath)234 try WallpaperManager.shared.saveToFolder(from: localURL, to: destFolder)235 }236 237 } catch {238 self.errorMessage = "Failed to set wallpaper: \(error.localizedDescription)"239 }240 }241 242 func setLocalWallpaper(_ wallpaper: LocalWallpaper, on screen: NSScreen? = nil) async {243 do {244 try await MainActor.run {245 try WallpaperManager.shared.setDesktopImage(from: wallpaper.url, screen: screen)246 }247 248 let historyWallpaper = Wallpaper(249 id: wallpaper.id,250 url: wallpaper.url.path,251 path: wallpaper.url.path,252 resolution: wallpaper.resolution,253 purity: "local",254 file_size: Int(wallpaper.fileSize),255 thumbs: Wallpaper.Thumbs(256 large: wallpaper.url.path,257 original: wallpaper.url.path,258 small: wallpaper.url.path259 )260 )261 historyManager.addToHistory(historyWallpaper)262 263 } catch {264 self.errorMessage = "Failed to set wallpaper: \(error.localizedDescription)"265 }266 }267 268 func downloadWallpaper(_ wallpaper: Wallpaper) async {269 do {270 let localURL = try await WallpaperManager.shared.download(from: URL(string: wallpaper.path)!)271 272 273 let destFolder = URL(fileURLWithPath: saveFolderPath)274 try WallpaperManager.shared.saveToFolder(from: localURL, to: destFolder)275 276 } catch {277 self.errorMessage = "Failed to download: \(error.localizedDescription)"278 }279 }280 281 func selectSaveFolder() {282 283 284 MenuController.shared?.beginModalPresentation()285 NSApp.activate(ignoringOtherApps: true)286287 let panel = NSOpenPanel()288 panel.canChooseFiles = false289 panel.canChooseDirectories = true290 panel.canCreateDirectories = true291 panel.allowsMultipleSelection = false292 panel.directoryURL = URL(fileURLWithPath: saveFolderPath)293 panel.prompt = "Choose"294295 panel.begin { response in296 if response == .OK, let url = panel.url {297 self.saveFolderPath = url.path298 }299 MenuController.shared?.endModalPresentation()300 }301 }302 303 func quit() {304 NSApp.terminate(nil)305 }306}