AppState.swift

9.0 KB · raw

1import Foundation2import Combine3import AppKit45enum AppTab {6    case explore7    case local8    case favorites9    case history10    case settings11}1213@MainActor14class AppState: ObservableObject {15    // Navigation16    @Published var activeTab: AppTab = .explore17    18    // Data Sources19    @Published var wallpapers: [Wallpaper] = []20    @Published var favoritesManager = FavoritesManager.shared21    @Published var historyManager = HistoryManager.shared22    23    // Search & Filter24    @Published var searchText: String = ""25    @Published var selectedResolution: String = ""26    @Published var selectedRatio: String = ""27    @Published var isLoading: Bool = false28    @Published var errorMessage: String?29    30    // Pagination31    @Published var canLoadMore = true32    private var currentPage = 133    private var isFetchingMore = false34    35    // Settings36    @Published var saveToFolderEnabled: Bool {37        didSet { UserDefaults.standard.set(saveToFolderEnabled, forKey: "saveToFolderEnabled") }38    }39    @Published var saveFolderPath: String {40        didSet { UserDefaults.standard.set(saveFolderPath, forKey: "saveFolderPath") }41    }42    @Published var launchAtLogin: Bool = AutostartManager.shared.isEnabled {43        didSet { AutostartManager.shared.isEnabled = launchAtLogin }44    }45    46    // Filters47    @Published var categories: (general: Bool, anime: Bool, people: Bool) = (true, true, true)48    @Published var purity: (sfw: Bool, sketchy: Bool) = (true, false)49    50    let availableResolutions = ["Any", "1920x1080", "2560x1440", "3840x2160", "5120x2880"]51    let availableRatios = ["Any", "16x9", "16x10", "21x9", "32x9", "4x3"]52    53    private let api = WallhavenAPI()54    private var cancellables = Set<AnyCancellable>()55    56    init() {57        // Load settings58        self.saveToFolderEnabled = UserDefaults.standard.bool(forKey: "saveToFolderEnabled")59        if let path = UserDefaults.standard.string(forKey: "saveFolderPath") {60            self.saveFolderPath = path61        } else {62            let pictures = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first!63            self.saveFolderPath = pictures.appendingPathComponent("Haven").path64        }65        66        // Debounce search67        $searchText68            .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)69            .sink { [weak self] _ in70                Task { [weak self] in71                    await self?.fetchWallpapers()72                }73            }74            .store(in: &cancellables)75            76        // Observe filters77        $categories78            .map { _ in () }79            .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)80            .sink { [weak self] _ in Task { await self?.fetchWallpapers() } }81            .store(in: &cancellables)82            83        $purity84            .map { _ in () }85            .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)86            .sink { [weak self] _ in Task { await self?.fetchWallpapers() } }87            .store(in: &cancellables)88            89        updateLocalWallpapersMonitoring()90        91        $saveToFolderEnabled92            .dropFirst()93            .sink { [weak self] _ in94                self?.updateLocalWallpapersMonitoring()95            }96            .store(in: &cancellables)97            98        $saveFolderPath99            .dropFirst()100            .sink { [weak self] _ in101                self?.updateLocalWallpapersMonitoring()102            }103            .store(in: &cancellables)104    }105    106    private func updateLocalWallpapersMonitoring() {107        if saveToFolderEnabled {108            LocalWallpapersManager.shared.startMonitoring(folderPath: saveFolderPath)109        } else {110            LocalWallpapersManager.shared.stopMonitoring()111        }112    }113    114    func fetchWallpapers(reset: Bool = true, refreshing: Bool = false) async {115        if reset {116            if !refreshing {117                isLoading = true118                wallpapers = []119            }120            errorMessage = nil121            currentPage = 1122            canLoadMore = true123        } else {124            guard !isFetchingMore && canLoadMore else { return }125            isFetchingMore = true126        }127        128        do {129            let resolutions = selectedResolution == "Any" || selectedResolution.isEmpty ? [] : [selectedResolution]130            let ratios = selectedRatio == "Any" || selectedRatio.isEmpty ? [] : [selectedRatio]131            132            // Construct bitstrings133            let catStr = "\(categories.general ? "1" : "0")\(categories.anime ? "1" : "0")\(categories.people ? "1" : "0")"134            // NSFW is intentionally unsupported; the third purity bit is always 0.135            let purityStr = "\(purity.sfw ? "1" : "0")\(purity.sketchy ? "1" : "0")0"136            137            let (newWallpapers, meta) = try await api.searchWallpapers(138                query: searchText,139                categories: catStr,140                purity: purityStr,141                resolutions: resolutions,142                ratios: ratios,143                limit: 24,144                page: currentPage145            )146            147            if reset {148                self.wallpapers = newWallpapers149            } else {150                self.wallpapers.append(contentsOf: newWallpapers)151            }152            153            if let meta = meta {154                self.canLoadMore = meta.current_page < meta.last_page155                self.currentPage = meta.current_page + 1156            } else {157                self.canLoadMore = !newWallpapers.isEmpty158                if !newWallpapers.isEmpty { self.currentPage += 1 }159            }160            161        } catch {162            if reset {163                self.errorMessage = error.localizedDescription164            }165        }166        167        if reset {168            isLoading = false169        } else {170            isFetchingMore = false171        }172    }173    174    func loadMore() async {175        await fetchWallpapers(reset: false)176    }177    178    func setWallpaper(_ wallpaper: Wallpaper, on screen: NSScreen? = nil) async {179        do {180            let localURL = try await WallpaperManager.shared.download(from: URL(string: wallpaper.path)!)181            182            try await MainActor.run {183                try WallpaperManager.shared.setDesktopImage(from: localURL, screen: screen)184            }185            186            // Add to history187            historyManager.addToHistory(wallpaper)188            189            if saveToFolderEnabled {190                let destFolder = URL(fileURLWithPath: saveFolderPath)191                try WallpaperManager.shared.saveToFolder(from: localURL, to: destFolder)192            }193            194        } catch {195            self.errorMessage = "Failed to set wallpaper: \(error.localizedDescription)"196        }197    }198    199    func setLocalWallpaper(_ wallpaper: LocalWallpaper, on screen: NSScreen? = nil) async {200        do {201            try await MainActor.run {202                try WallpaperManager.shared.setDesktopImage(from: wallpaper.url, screen: screen)203            }204            205            let historyWallpaper = Wallpaper(206                id: wallpaper.id,207                url: wallpaper.url.path,208                path: wallpaper.url.path,209                resolution: wallpaper.resolution,210                purity: "local",211                file_size: Int(wallpaper.fileSize),212                thumbs: Wallpaper.Thumbs(213                    large: wallpaper.url.path,214                    original: wallpaper.url.path,215                    small: wallpaper.url.path216                )217            )218            historyManager.addToHistory(historyWallpaper)219            220        } catch {221            self.errorMessage = "Failed to set wallpaper: \(error.localizedDescription)"222        }223    }224    225    func downloadWallpaper(_ wallpaper: Wallpaper) async {226         do {227            let localURL = try await WallpaperManager.shared.download(from: URL(string: wallpaper.path)!)228            229            // Always save to folder if download requested, or ensure save folder is used230            let destFolder = URL(fileURLWithPath: saveFolderPath)231            try WallpaperManager.shared.saveToFolder(from: localURL, to: destFolder)232            233        } catch {234            self.errorMessage = "Failed to download: \(error.localizedDescription)"235        }236    }237    238    func selectSaveFolder() {239        // Hold the transient popover open; without this the picker steals focus240        // and the popover closes before a folder can be chosen.241        MenuController.shared?.beginModalPresentation()242        NSApp.activate(ignoringOtherApps: true)243244        let panel = NSOpenPanel()245        panel.canChooseFiles = false246        panel.canChooseDirectories = true247        panel.canCreateDirectories = true248        panel.allowsMultipleSelection = false249        panel.directoryURL = URL(fileURLWithPath: saveFolderPath)250        panel.prompt = "Choose"251252        panel.begin { response in253            if response == .OK, let url = panel.url {254                self.saveFolderPath = url.path255            }256            MenuController.shared?.endModalPresentation()257        }258    }259    260    func quit() {261        NSApp.terminate(nil)262    }263}