Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/Wallpaper/BingAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ struct BingImage: Codable, Hashable, Identifiable {
enum WallpaperError: LocalizedError {
case noImages
case downloadFailed
case invalidURL

var errorDescription: String? {
switch self {
case .noImages: return "No images found"
case .downloadFailed: return "Failed to download image"
case .invalidURL: return "Invalid image URL"
}
}
}
15 changes: 14 additions & 1 deletion Sources/Wallpaper/PreferencesStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ struct WallpaperPreferences: Codable {
final class PreferencesStore {
static let shared = PreferencesStore()
private(set) var preferences = WallpaperPreferences()
/// Favorites in newest-first order, kept in sync on mutation so callers never re-sort
private(set) var sortedFavorites: [BingImage] = []
let fileURL: URL

init(fileURL: URL? = nil) {
Expand All @@ -20,12 +22,17 @@ final class PreferencesStore {

func load() {
let fm = FileManager.default
defer { rebuildSortedFavorites() }
guard fm.fileExists(atPath: fileURL.path) else { return }
do {
let data = try Data(contentsOf: fileURL)
preferences = try JSONDecoder().decode(WallpaperPreferences.self, from: data)
} catch {
// If corrupted, start fresh
// Start fresh, but move the unreadable file aside first: otherwise the next
// save() would overwrite it and destroy every favorite and dislike for good.
let backupURL = fileURL.appendingPathExtension("corrupt")
try? fm.removeItem(at: backupURL)
try? fm.moveItem(at: fileURL, to: backupURL)
preferences = WallpaperPreferences()
}
}
Expand Down Expand Up @@ -63,11 +70,13 @@ final class PreferencesStore {
func addFavorite(_ image: BingImage) {
guard !preferences.favorites.contains(where: { $0.startdate == image.startdate }) else { return }
preferences.favorites.append(image)
rebuildSortedFavorites()
save()
}

func removeFavorite(_ image: BingImage) {
preferences.favorites.removeAll { $0.startdate == image.startdate }
rebuildSortedFavorites()
save()
}

Expand All @@ -78,4 +87,8 @@ final class PreferencesStore {
func favoriteDates() -> Set<String> {
Set(preferences.favorites.map(\.startdate))
}

private func rebuildSortedFavorites() {
sortedFavorites = preferences.favorites.sorted { $0.startdate > $1.startdate }
}
}
22 changes: 20 additions & 2 deletions Sources/Wallpaper/UpdateChecker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,11 @@ final class UpdateChecker {
// MARK: - Semver Comparison

nonisolated static func isNewer(remote: String, than current: String) -> Bool {
let remoteParts = remote.split(separator: ".").compactMap { Int($0) }
let currentParts = current.split(separator: ".").compactMap { Int($0) }
// Parse strictly: dropping unparseable components would shift the remaining ones
// into the wrong significance position (e.g. "1.beta.5" comparing 5 against minor).
guard let remoteParts = versionComponents(remote),
let currentParts = versionComponents(current)
else { return false }
let count = max(remoteParts.count, currentParts.count)
for i in 0..<count {
let r = i < remoteParts.count ? remoteParts[i] : 0
Expand All @@ -195,6 +198,21 @@ final class UpdateChecker {
}
return false
}

/// Split "1.2.3" into [1, 2, 3], tolerating a trailing pre-release suffix such as
/// "3-rc1". Returns nil if any component has no leading digits.
private nonisolated static func versionComponents(_ version: String) -> [Int]? {
let parts = version.split(separator: ".")
guard !parts.isEmpty else { return nil }

var numbers: [Int] = []
for part in parts {
let digits = part.prefix { $0.isNumber }
guard let value = Int(digits) else { return nil }
numbers.append(value)
}
return numbers
}
}

// MARK: - Download Progress
Expand Down
13 changes: 9 additions & 4 deletions Sources/Wallpaper/WallpaperApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ struct WallpaperApp: App {
toolbar
}
.frame(width: 320)
// The login item can be toggled in System Settings while we run, so re-read
// it every time the window opens instead of trusting the launch-time snapshot
.onAppear { launchAtLogin = SMAppService.mainApp.status == .enabled }
}
.menuBarExtraStyle(.window)
}
Expand Down Expand Up @@ -330,11 +333,13 @@ struct WallpaperApp: App {
Spacer()

Button {
launchAtLogin.toggle()
do {
if launchAtLogin { try SMAppService.mainApp.register() }
else { try SMAppService.mainApp.unregister() }
} catch { launchAtLogin.toggle() }
if launchAtLogin { try SMAppService.mainApp.unregister() }
else { try SMAppService.mainApp.register() }
} catch {
// Fall through β€” the status re-read below reports what actually happened
}
launchAtLogin = SMAppService.mainApp.status == .enabled
} label: {
Image(systemName: launchAtLogin ? "checkmark.circle.fill" : "circle")
}
Expand Down
Loading