From cfb40f624792338232f3889bde7454b5db2648b6 Mon Sep 17 00:00:00 2001 From: harsha balakrishnan Date: Tue, 6 Oct 2026 13:40:35 +0530 Subject: [PATCH 1/5] refactor(config): share bookmark helpers without changing Space keys Deduplicate security-scoped bookmark create/resolve paths and SpaceConfig copies. Assign still keys configs by spaceKey; unused fallbackURL is dropped from resolveBookmark. --- Sources/Lively/Logic/ConfigStore.swift | 156 +++++++++++-------------- Sources/Lively/UI/ScreenCardView.swift | 34 +----- 2 files changed, 74 insertions(+), 116 deletions(-) diff --git a/Sources/Lively/Logic/ConfigStore.swift b/Sources/Lively/Logic/ConfigStore.swift index 1b5ef16..1d9b3d9 100644 --- a/Sources/Lively/Logic/ConfigStore.swift +++ b/Sources/Lively/Logic/ConfigStore.swift @@ -7,15 +7,24 @@ import Combine public struct SpaceConfig: Codable, Sendable { /// Composite key: "displayID:desktopImageURL.absoluteString" public let spaceKey: String - + /// The dynamic wallpaper configuration. public let dynamicWallpaper: DynamicWallpaper - + /// Bookmarks for all relevant URLs. /// Key: "static", "light", "dark". public let bookmarks: [String: Data] - + public let addedAt: Date + + func replacing(dynamicWallpaper: DynamicWallpaper? = nil, bookmarks: [String: Data]? = nil) -> SpaceConfig { + SpaceConfig( + spaceKey: spaceKey, + dynamicWallpaper: dynamicWallpaper ?? self.dynamicWallpaper, + bookmarks: bookmarks ?? self.bookmarks, + addedAt: addedAt + ) + } } // MARK: - ConfigStore @@ -66,33 +75,15 @@ public class ConfigStore: ObservableObject { AppMetrics.shared.recordWallpaperApplied() var bookmarks: [String: Data] = [:] - - func bookmark(for url: URL?, key: String) -> Data? { - guard let url else { return nil } - do { - return try url.bookmarkData( - options: [.withSecurityScope, .securityScopeAllowOnlyReadAccess], - includingResourceValuesForKeys: nil, - relativeTo: nil - ) - } catch { - errors.send(.bookmarkCreationFailed(key)) - LivelyLogger.config.error("Failed to create bookmark for \(key): \(error.localizedDescription)") - return nil - } - } - - if let url = dynamicWallpaper.staticURL { - guard let data = bookmark(for: url, key: "static") else { return } - bookmarks["static"] = data - } - if let url = dynamicWallpaper.lightURL { - guard let data = bookmark(for: url, key: "light") else { return } - bookmarks["light"] = data - } - if let url = dynamicWallpaper.darkURL { - guard let data = bookmark(for: url, key: "dark") else { return } - bookmarks["dark"] = data + let sources: [(String, URL?)] = [ + ("static", dynamicWallpaper.staticURL), + ("light", dynamicWallpaper.lightURL), + ("dark", dynamicWallpaper.darkURL), + ] + for (bookmarkKey, url) in sources { + guard let url else { continue } + guard let data = makeBookmarkData(for: url, key: bookmarkKey) else { return } + bookmarks[bookmarkKey] = data } let config = SpaceConfig( @@ -130,8 +121,18 @@ public class ConfigStore: ObservableObject { return nil } - var isStale = false + return resolveURL(fromBookmark: data, spaceKey: spaceKey, bookmarkKey: bookmarkKey, verbose: true) + } + + /// Resolves a specific bookmark key directly. + /// Used by secondary components like VideoThumbnailView to gain access. + public func resolveBookmark(for spaceKey: String, bookmarkKey: String) -> URL? { + guard let data = configs[spaceKey]?.bookmarks[bookmarkKey] else { return nil } + return resolveURL(fromBookmark: data, spaceKey: spaceKey, bookmarkKey: bookmarkKey, verbose: false) + } + private func resolveURL(fromBookmark data: Data, spaceKey: String, bookmarkKey: String, verbose: Bool) -> URL? { + var isStale = false do { let resolved = try URL( resolvingBookmarkData: data, @@ -139,13 +140,13 @@ public class ConfigStore: ObservableObject { relativeTo: nil, bookmarkDataIsStale: &isStale ) - if isStale { - LivelyLogger.config.info("Refreshing stale bookmark for \(bookmarkKey)") + if verbose { LivelyLogger.config.info("Refreshing stale bookmark for \(bookmarkKey)") } refreshBookmark(spaceKey: spaceKey, bookmarkKey: bookmarkKey, url: resolved) } - - LivelyLogger.config.info("Resolved \(bookmarkKey) via security-scoped bookmark → \(resolved.lastPathComponent)") + if verbose { + LivelyLogger.config.info("Resolved \(bookmarkKey) via security-scoped bookmark → \(resolved.lastPathComponent)") + } return resolved } catch { do { @@ -156,44 +157,43 @@ public class ConfigStore: ObservableObject { relativeTo: nil, bookmarkDataIsStale: &legacyStale ) - LivelyLogger.config.info("Legacy bookmark resolved for \(bookmarkKey), upgrading to security-scoped") + if verbose { + LivelyLogger.config.info("Legacy bookmark resolved for \(bookmarkKey), upgrading to security-scoped") + } refreshBookmark(spaceKey: spaceKey, bookmarkKey: bookmarkKey, url: legacyResolved) return legacyResolved } catch let legacyError { - LivelyLogger.config.error("Bookmark resolution failed for \(bookmarkKey). Primary error: \(error.localizedDescription). Legacy fallback error: \(legacyError.localizedDescription)") + if verbose { + LivelyLogger.config.error("Bookmark resolution failed for \(bookmarkKey). Primary error: \(error.localizedDescription). Legacy fallback error: \(legacyError.localizedDescription)") + } return nil } } } - - /// Resolves a specific bookmark key directly. - /// Used by secondary components like VideoThumbnailView to gain access. - public func resolveBookmark(for spaceKey: String, bookmarkKey: String, fallbackURL: URL?) -> URL? { - guard let config = configs[spaceKey] else { return nil } - guard let data = config.bookmarks[bookmarkKey] else { return nil } - - var isStale = false + + private func makeBookmarkData(for url: URL, key: String) -> Data? { do { - let resolved = try URL(resolvingBookmarkData: data, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale) - if isStale { refreshBookmark(spaceKey: spaceKey, bookmarkKey: bookmarkKey, url: resolved) } - return resolved + return try Self.securityScopedBookmarkData(for: url) } catch { - do { - var legacyStale = false - let legacyResolved = try URL(resolvingBookmarkData: data, options: [], relativeTo: nil, bookmarkDataIsStale: &legacyStale) - refreshBookmark(spaceKey: spaceKey, bookmarkKey: bookmarkKey, url: legacyResolved) - return legacyResolved - } catch { - return nil - } + errors.send(.bookmarkCreationFailed(key)) + LivelyLogger.config.error("Failed to create bookmark for \(key): \(error.localizedDescription)") + return nil } } - + + nonisolated private static func securityScopedBookmarkData(for url: URL) throws -> Data { + try url.bookmarkData( + options: [.withSecurityScope, .securityScopeAllowOnlyReadAccess], + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + } + /// Re-creates a bookmark for a URL with security scope. private func refreshBookmark(spaceKey: String, bookmarkKey: String, url: URL) { let didStart = url.startAccessingSecurityScopedResource() defer { if didStart { url.stopAccessingSecurityScopedResource() } } - + guard let config = configs[spaceKey] else { errors.send(.bookmarkRefreshFailed(bookmarkKey)) LivelyLogger.config.error("No config found while refreshing bookmark for \(bookmarkKey)") @@ -202,31 +202,22 @@ public class ConfigStore: ObservableObject { let newData: Data do { - newData = try url.bookmarkData( - options: [.withSecurityScope, .securityScopeAllowOnlyReadAccess], - includingResourceValuesForKeys: nil, - relativeTo: nil - ) + newData = try Self.securityScopedBookmarkData(for: url) } catch { errors.send(.bookmarkRefreshFailed(bookmarkKey)) LivelyLogger.config.error("Failed to refresh bookmark for \(bookmarkKey): \(error.localizedDescription)") return } - + var updatedBookmarks = config.bookmarks updatedBookmarks[bookmarkKey] = newData // A stale bookmark usually means the file was moved or renamed; keep the // stored path in step so the UI shows the real name. - let wallpaper = Self.wallpaper(config.dynamicWallpaper, replacing: bookmarkKey, with: url) - - let refreshed = SpaceConfig( - spaceKey: config.spaceKey, - dynamicWallpaper: wallpaper, - bookmarks: updatedBookmarks, - addedAt: config.addedAt + configs[spaceKey] = config.replacing( + dynamicWallpaper: Self.wallpaper(config.dynamicWallpaper, replacing: bookmarkKey, with: url), + bookmarks: updatedBookmarks ) - configs[spaceKey] = refreshed persist() LivelyLogger.config.info("Bookmark upgraded to security-scoped for \(bookmarkKey)") } @@ -254,13 +245,7 @@ public class ConfigStore: ObservableObject { guard updated != existing.dynamicWallpaper else { return } - let refreshed = SpaceConfig( - spaceKey: existing.spaceKey, - dynamicWallpaper: updated, - bookmarks: existing.bookmarks, - addedAt: existing.addedAt - ) - configs[key] = refreshed + configs[key] = existing.replacing(dynamicWallpaper: updated) persist() } @@ -414,16 +399,15 @@ public class ConfigStore: ObservableObject { private func isConfigPlayable(_ config: SpaceConfig) -> Bool { let wallpaper = config.dynamicWallpaper + func playable(_ url: URL?, key: String) -> Bool { + guard let url, isValidLivelyVideoFile(url), config.bookmarks[key] != nil else { return false } + return true + } switch wallpaper.mode { case .staticVideo: - guard let url = wallpaper.staticURL, - isValidLivelyVideoFile(url), - config.bookmarks["static"] != nil else { return false } - return true + return playable(wallpaper.staticURL, key: "static") case .appearance: - let hasLight = wallpaper.lightURL.map { isValidLivelyVideoFile($0) && config.bookmarks["light"] != nil } ?? false - let hasDark = wallpaper.darkURL.map { isValidLivelyVideoFile($0) && config.bookmarks["dark"] != nil } ?? false - return hasLight || hasDark + return playable(wallpaper.lightURL, key: "light") || playable(wallpaper.darkURL, key: "dark") } } } diff --git a/Sources/Lively/UI/ScreenCardView.swift b/Sources/Lively/UI/ScreenCardView.swift index cd2bbc0..7415df3 100644 --- a/Sources/Lively/UI/ScreenCardView.swift +++ b/Sources/Lively/UI/ScreenCardView.swift @@ -126,7 +126,7 @@ struct ScreenCardView: View { clearDark() } ) - .transition(modeTransition) + .transition(LivelyBrand.contentTransition) } } .padding(.horizontal, LivelyBrand.Spacing.md) @@ -510,29 +510,12 @@ struct ScreenCardView: View { } } - private var modeTransition: AnyTransition { - LivelyBrand.contentTransition - } - // MARK: Drop Zone private func refreshResolvedURLs() { - let wallpaper = currentWallpaper - resolvedStaticURL = configStore.resolveBookmark( - for: space.spaceKey, - bookmarkKey: "static", - fallbackURL: wallpaper.staticURL - ) - resolvedLightURL = configStore.resolveBookmark( - for: space.spaceKey, - bookmarkKey: "light", - fallbackURL: wallpaper.lightURL - ) - resolvedDarkURL = configStore.resolveBookmark( - for: space.spaceKey, - bookmarkKey: "dark", - fallbackURL: wallpaper.darkURL - ) + resolvedStaticURL = configStore.resolveBookmark(for: space.spaceKey, bookmarkKey: "static") + resolvedLightURL = configStore.resolveBookmark(for: space.spaceKey, bookmarkKey: "light") + resolvedDarkURL = configStore.resolveBookmark(for: space.spaceKey, bookmarkKey: "dark") } // C-1 fix: DropZoneView owns its own @ViewState so each instance has @@ -587,7 +570,6 @@ struct ScreenCardView: View { updated.darkURL = url } configStore.assign(dynamicWallpaper: updated, toSpaceKey: space.spaceKey) - AppMetrics.shared.recordWallpaperApplied() showPlayingBanner = true AccessibilityNotification.Announcement("Playing on this display.").post() if wasFirstTime || !preferences.hasSeenSpacesCoach { @@ -629,14 +611,6 @@ struct ScreenCardView: View { AccessibilityNotification.Announcement("Wallpaper applied to all displays.").post() } - private func showSuccessMessage() { - showPlayingBanner = true - if !preferences.hasSeenSpacesCoach { - showSpacesCoach = true - } - AccessibilityNotification.Announcement("Playing on this display.").post() - } - private func showError(_ message: String) { errorMessage = message AccessibilityNotification.Announcement(message).post() From 8b61f35141b48b307c1c8a8dbde4f6caf739cfa9 Mon Sep 17 00:00:00 2001 From: harsha balakrishnan Date: Tue, 6 Oct 2026 13:40:41 +0530 Subject: [PATCH 2/5] refactor(ui): drop unused glass, about, and brand surfaces Remove dead GlassEffectType/button helpers, unused About compact/legacy layout, unused brand gradients, unused hover state, and duplicate AppMetrics calls already performed by ConfigStore.assign. --- Sources/Lively/UI/AboutView.swift | 96 +------------------ Sources/Lively/UI/DisplaysView.swift | 4 - Sources/Lively/UI/GlassEffect.swift | 92 +----------------- Sources/Lively/UI/LibraryView.swift | 2 - .../Lively/UI/LibraryWindowPresenter.swift | 1 - Sources/Lively/UI/LivelyBrand.swift | 18 ---- Sources/Lively/UI/PreferencesView.swift | 16 ++-- 7 files changed, 9 insertions(+), 220 deletions(-) diff --git a/Sources/Lively/UI/AboutView.swift b/Sources/Lively/UI/AboutView.swift index 6616f0a..eeff038 100644 --- a/Sources/Lively/UI/AboutView.swift +++ b/Sources/Lively/UI/AboutView.swift @@ -2,12 +2,7 @@ import SwiftUI import AppKit public struct AboutView: View { - /// Compact layout for the Settings sidebar pane (matches design collage). - public var compact: Bool - - public init(compact: Bool = false) { - self.compact = compact - } + public init() {} private var appVersion: String { let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" @@ -16,16 +11,6 @@ public struct AboutView: View { } public var body: some View { - if compact { - compactBody - } else { - legacyBody - } - } - - // MARK: - Settings → About (collage) - - private var compactBody: some View { VStack(spacing: LivelyBrand.Spacing.lg) { VStack(spacing: LivelyBrand.Spacing.md) { ZStack { @@ -159,83 +144,4 @@ public struct AboutView: View { private var privacyURL: URL? { URL(string: "https://github.com/harshabala/Lively/blob/master/README.md#privacy") } - - // MARK: - Legacy layout (if reused elsewhere) - - private var legacyBody: some View { - HStack(alignment: .top, spacing: LivelyBrand.Spacing.xl) { - VStack(alignment: .leading, spacing: LivelyBrand.Spacing.lg) { - HStack(alignment: .top, spacing: LivelyBrand.Spacing.lg) { - if let appIcon = NSImage(named: NSImage.applicationIconName) { - Image(nsImage: appIcon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 64, height: 64) - .shadow( - color: LivelyBrand.Shadow.color, - radius: LivelyBrand.Shadow.radius, - x: LivelyBrand.Shadow.x, - y: LivelyBrand.Shadow.y - ) - } else { - Image(systemName: "play.tv.fill") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 64, height: 64) - .foregroundStyle(LivelyBrand.primary) - } - - VStack(alignment: .leading, spacing: 4) { - Text("Lively") - .font(LivelyBrand.Typography.title) - .foregroundStyle(LivelyBrand.foreground) - - Text(appVersion) - .font(LivelyBrand.Typography.mono) - .foregroundStyle(LivelyBrand.mutedForeground) - - Text("Video wallpapers for each Space on your Mac.") - .font(LivelyBrand.Typography.caption) - .foregroundStyle(LivelyBrand.mutedForeground) - .padding(.top, LivelyBrand.Spacing.sm) - } - } - - if let releasesURL = URL(string: "https://github.com/harshabala/Lively/releases") { - Link("View releases on GitHub", destination: releasesURL) - .font(LivelyBrand.Typography.caption) - } - } - - Divider() - .overlay(LivelyBrand.border.opacity(0.35)) - .padding(.horizontal, LivelyBrand.Spacing.sm) - - VStack(alignment: .leading, spacing: LivelyBrand.Spacing.lg) { - VStack(alignment: .leading, spacing: 4) { - Text("Formats") - .font(LivelyBrand.Typography.caption.weight(.bold)) - .foregroundStyle(LivelyBrand.foreground.opacity(0.8)) - Text("H.264 · HEVC (H.265)") - .font(LivelyBrand.Typography.caption) - .foregroundStyle(LivelyBrand.mutedForeground) - Text("MP4 · MOV · M4V · Up to 4K") - .font(LivelyBrand.Typography.caption) - .foregroundStyle(LivelyBrand.mutedForeground) - } - - VStack(alignment: .leading, spacing: 4) { - Text("File Size") - .font(LivelyBrand.Typography.caption.weight(.bold)) - .foregroundStyle(LivelyBrand.foreground.opacity(0.8)) - Text("No limit. Hardware-accelerated decoding for full 4K playback.") - .font(LivelyBrand.Typography.caption) - .foregroundStyle(LivelyBrand.mutedForeground) - .lineLimit(2) - } - } - .padding(.top, 4) - } - .frame(maxWidth: .infinity, alignment: .leading) - } } diff --git a/Sources/Lively/UI/DisplaysView.swift b/Sources/Lively/UI/DisplaysView.swift index 02c46c9..d0ea2cd 100644 --- a/Sources/Lively/UI/DisplaysView.swift +++ b/Sources/Lively/UI/DisplaysView.swift @@ -9,7 +9,6 @@ public struct DisplaysView: View { @ObservedObject private var preferences = AppPreferences.shared @Environment(\.accessibilityReduceMotion) private var reduceMotion @ViewState private var displayedDays: Int = 0 - @ViewState private var isLibraryButtonHovered = false public init(spaceMonitor: SpaceMonitor, configStore: ConfigStore) { self.spaceMonitor = spaceMonitor @@ -237,9 +236,6 @@ public struct DisplaysView: View { RoundedRectangle(cornerRadius: LivelyBrand.Radius.md) .strokeBorder(LivelyBrand.border.opacity(0.35), lineWidth: 1) ) - .onHover { hovering in - isLibraryButtonHovered = hovering - } .accessibilityElement(children: .combine) .accessibilityLabel("Open Wallpaper Library") .accessibilityHint("Open your reusable wallpaper library to add or apply videos.") diff --git a/Sources/Lively/UI/GlassEffect.swift b/Sources/Lively/UI/GlassEffect.swift index 0f87a27..e0d39cf 100644 --- a/Sources/Lively/UI/GlassEffect.swift +++ b/Sources/Lively/UI/GlassEffect.swift @@ -1,12 +1,10 @@ import SwiftUI import AppKit -// MARK: - Glass Effect Implementation - struct GlassEffect: NSViewRepresentable { let material: NSVisualEffectView.Material let blendingMode: NSVisualEffectView.BlendingMode - + func makeNSView(context: Context) -> NSVisualEffectView { let view = NSVisualEffectView() view.material = material @@ -14,95 +12,9 @@ struct GlassEffect: NSViewRepresentable { view.state = .active return view } - + func updateNSView(_ nsView: NSVisualEffectView, context: Context) { nsView.material = material nsView.blendingMode = blendingMode } } - -// MARK: - Type System - -struct GlassEffectType { - let material: NSVisualEffectView.Material - let tintColor: Color? - let tintOpacity: Double - - static let regular = GlassEffectType(material: .headerView, tintColor: nil, tintOpacity: 0.10) - static let thick = GlassEffectType(material: .contentBackground, tintColor: nil, tintOpacity: 0.10) - static let thin = GlassEffectType(material: .hudWindow, tintColor: nil, tintOpacity: 0.10) - static let ultraThin = GlassEffectType(material: .underPageBackground, tintColor: nil, tintOpacity: 0.10) - static let clear = GlassEffectType(material: .fullScreenUI, tintColor: nil, tintOpacity: 0.10) - - func tint(_ color: Color?) -> GlassEffectType { - GlassEffectType(material: material, tintColor: color, tintOpacity: tintOpacity) - } - - func opacity(_ value: Double) -> GlassEffectType { - GlassEffectType(material: material, tintColor: tintColor, tintOpacity: value) - } -} - -// MARK: - Extensions - -extension View { - func liquidGlass(_ type: GlassEffectType = .regular, in shape: some Shape) -> some View { - self.background( - ZStack { - GlassEffect(material: type.material, blendingMode: .withinWindow) - if let color = type.tintColor { - color.opacity(type.tintOpacity) - } - } - .clipShape(shape) - ) - } -} - -// MARK: - Button Style - -struct GlassButtonStyle: ButtonStyle { - @Environment(\.accessibilityReduceMotion) private var reduceMotion - var tint: Color? = nil - var isProminent = false - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(.horizontal, LivelyBrand.Spacing.md) - .padding(.vertical, LivelyBrand.Spacing.sm) - .frame(minHeight: LivelyBrand.Spacing.controlMin) - .foregroundStyle(isProminent ? .white : .primary) - .background( - ZStack { - GlassEffect(material: .headerView, blendingMode: .withinWindow) - if let tint = tint { - tint.opacity(configuration.isPressed ? 0.26 : isProminent ? 0.92 : 0.14) - } else { - LivelyBrand.foreground.opacity(configuration.isPressed ? 0.1 : 0.0) - } - } - .cornerRadius(LivelyBrand.Radius.navItem) - ) - .scaleEffect(configuration.isPressed ? 0.97 : 1.0) - .opacity(configuration.isPressed ? 0.92 : 1.0) - .animation(reduceMotion ? nil : LivelyBrand.Motion.press, value: configuration.isPressed) - } -} - -extension ButtonStyle where Self == GlassButtonStyle { - static var glass: GlassButtonStyle { GlassButtonStyle() } -} - -extension GlassButtonStyle { - func tint(_ color: Color) -> GlassButtonStyle { - var copy = self - copy.tint = color - return copy - } - - func prominent(_ value: Bool = true) -> GlassButtonStyle { - var copy = self - copy.isProminent = value - return copy - } -} diff --git a/Sources/Lively/UI/LibraryView.swift b/Sources/Lively/UI/LibraryView.swift index 7ad8c25..c5444a8 100644 --- a/Sources/Lively/UI/LibraryView.swift +++ b/Sources/Lively/UI/LibraryView.swift @@ -241,7 +241,6 @@ public struct LibraryView: View { wallpaperConfig.mode = .staticVideo wallpaperConfig.staticURL = localURL configStore.assign(dynamicWallpaper: wallpaperConfig, toSpaceKey: spaceKey) - AppMetrics.shared.recordWallpaperApplied() addError = nil } @@ -252,7 +251,6 @@ public struct LibraryView: View { return } configStore.applyStaticWallpaper(localURL, toAllSpaceKeys: keys) - AppMetrics.shared.recordWallpaperApplied() addError = nil } diff --git a/Sources/Lively/UI/LibraryWindowPresenter.swift b/Sources/Lively/UI/LibraryWindowPresenter.swift index eefc034..965721a 100644 --- a/Sources/Lively/UI/LibraryWindowPresenter.swift +++ b/Sources/Lively/UI/LibraryWindowPresenter.swift @@ -37,4 +37,3 @@ public final class LibraryWindowController: NSObject, NSWindowDelegate { } } - diff --git a/Sources/Lively/UI/LivelyBrand.swift b/Sources/Lively/UI/LivelyBrand.swift index 34d231a..81712ac 100644 --- a/Sources/Lively/UI/LivelyBrand.swift +++ b/Sources/Lively/UI/LivelyBrand.swift @@ -12,34 +12,16 @@ enum LivelyBrand { // Semantic neutrals — prefer system colors so light/dark inherit correctly. static var background: Color { Color(nsColor: .windowBackgroundColor) } - static var backgroundLifted: Color { Color(nsColor: .controlBackgroundColor) } static var card: Color { Color(nsColor: .controlBackgroundColor) } /// Elevated/control surface fill (not brand accent). Prefer this over inventing greys. static var controlFill: Color { Color(nsColor: .controlBackgroundColor) } static var foreground: Color { Color(nsColor: .labelColor) } static var mutedForeground: Color { Color(nsColor: .secondaryLabelColor) } static var border: Color { Color(nsColor: .separatorColor) } - static var logBackground: Color { Color(nsColor: .textBackgroundColor) } /// Brand-tint fill for selected sidebar rows only (interactive selection language). static var selectionFill: Color { primary.opacity(0.13) } - static var backgroundGradient: LinearGradient { - LinearGradient( - colors: [background, backgroundLifted], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - } - - static var accentGradient: LinearGradient { - LinearGradient( - colors: [primary, primarySoft], - startPoint: .leading, - endPoint: .trailing - ) - } - enum Spacing { static let tiny: CGFloat = 3 static let xs: CGFloat = 4 diff --git a/Sources/Lively/UI/PreferencesView.swift b/Sources/Lively/UI/PreferencesView.swift index faa8b8f..4bd768c 100644 --- a/Sources/Lively/UI/PreferencesView.swift +++ b/Sources/Lively/UI/PreferencesView.swift @@ -160,19 +160,19 @@ public struct PreferencesView: View { switch section { case .general: generalPane - .transition(paneTransition) + .transition(LivelyBrand.contentTransition) case .playback: playbackPane - .transition(paneTransition) + .transition(LivelyBrand.contentTransition) case .screenSetup: screenSetupPane - .transition(paneTransition) + .transition(LivelyBrand.contentTransition) case .logs: logsPane - .transition(paneTransition) + .transition(LivelyBrand.contentTransition) case .about: aboutPane - .transition(paneTransition) + .transition(LivelyBrand.contentTransition) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) @@ -181,10 +181,6 @@ public struct PreferencesView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - private var paneTransition: AnyTransition { - LivelyBrand.contentTransition - } - private func paneHeader(title: String, subtitle: String) -> some View { VStack(alignment: .leading, spacing: LivelyBrand.Spacing.xs) { Text(title) @@ -424,7 +420,7 @@ public struct PreferencesView: View { ScrollView { VStack(alignment: .leading, spacing: LivelyBrand.Spacing.lg) { paneHeader(title: "About", subtitle: SettingsSection.about.subtitle) - AboutView(compact: true) + AboutView() .padding(.horizontal, LivelyBrand.Spacing.lg) .padding(.bottom, LivelyBrand.Spacing.lg) } From 0d429ab22c1624379d539ccb1311bbe025bfd244 Mon Sep 17 00:00:00 2001 From: harsha balakrishnan Date: Tue, 6 Oct 2026 13:40:47 +0530 Subject: [PATCH 3/5] test: share ConfigStore fixtures and drop duplicate cases Keep the same assertions with less setup, folding static-mode appearance coverage into DynamicWallpaperTests. --- Tests/LivelyTests/ConfigStoreTests.swift | 93 ++++++++----------- Tests/LivelyTests/DynamicWallpaperTests.swift | 8 +- Tests/LivelyTests/VideoValidationTests.swift | 30 ------ 3 files changed, 40 insertions(+), 91 deletions(-) diff --git a/Tests/LivelyTests/ConfigStoreTests.swift b/Tests/LivelyTests/ConfigStoreTests.swift index 1280775..81f928a 100644 --- a/Tests/LivelyTests/ConfigStoreTests.swift +++ b/Tests/LivelyTests/ConfigStoreTests.swift @@ -5,23 +5,35 @@ import Foundation @MainActor struct ConfigStoreTests { + private func makeJSONURL() -> URL { + FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") + } + + private func makeVideo(suffix: String = "video") -> URL { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_\(suffix).mp4") + FileManager.default.createFile(atPath: url.path, contents: Data(), attributes: nil) + return url + } + + private func staticWallpaper(_ url: URL) -> DynamicWallpaper { + var wallpaper = DynamicWallpaper() + wallpaper.mode = .staticVideo + wallpaper.staticURL = url + return wallpaper + } + // MARK: - Static Video Mode @Test func assignAndRetrieve() async { - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") + let tempFile = makeJSONURL() defer { try? FileManager.default.removeItem(at: tempFile) } let configStore = ConfigStore(configFileURL: tempFile) let spaceKey = "test-display:file:///test/wallpaper.png" - let tempVideo = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_video.mp4") - FileManager.default.createFile(atPath: tempVideo.path, contents: Data(), attributes: nil) + let tempVideo = makeVideo() defer { try? FileManager.default.removeItem(at: tempVideo) } - var wallpaper = DynamicWallpaper() - wallpaper.mode = .staticVideo - wallpaper.staticURL = tempVideo - - configStore.assign(dynamicWallpaper: wallpaper, toSpaceKey: spaceKey) + configStore.assign(dynamicWallpaper: staticWallpaper(tempVideo), toSpaceKey: spaceKey) #expect(configStore.configs[spaceKey] != nil) #expect(configStore.configs[spaceKey]?.dynamicWallpaper.staticURL == tempVideo) @@ -29,20 +41,15 @@ struct ConfigStoreTests { } @Test func persistenceRoundTrip() async throws { - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") + let tempFile = makeJSONURL() defer { try? FileManager.default.removeItem(at: tempFile) } let configStore = ConfigStore(configFileURL: tempFile) let spaceKey = "test-display:file:///test/roundtrip.png" - let tempVideo = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_roundtrip.mp4") - FileManager.default.createFile(atPath: tempVideo.path, contents: Data(), attributes: nil) + let tempVideo = makeVideo(suffix: "roundtrip") defer { try? FileManager.default.removeItem(at: tempVideo) } - var wallpaper = DynamicWallpaper() - wallpaper.mode = .staticVideo - wallpaper.staticURL = tempVideo - - configStore.assign(dynamicWallpaper: wallpaper, toSpaceKey: spaceKey) + configStore.assign(dynamicWallpaper: staticWallpaper(tempVideo), toSpaceKey: spaceKey) configStore.flushPendingPersist() let freshStore = ConfigStore(configFileURL: tempFile) @@ -51,20 +58,14 @@ struct ConfigStoreTests { } @Test func remove() async { - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") + let tempFile = makeJSONURL() defer { try? FileManager.default.removeItem(at: tempFile) } let configStore = ConfigStore(configFileURL: tempFile) - let spaceKey = "test-display:file:///test/remove.png" - let tempVideo = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_remove.mp4") - FileManager.default.createFile(atPath: tempVideo.path, contents: Data(), attributes: nil) + let tempVideo = makeVideo(suffix: "remove") defer { try? FileManager.default.removeItem(at: tempVideo) } - var wallpaper = DynamicWallpaper() - wallpaper.mode = .staticVideo - wallpaper.staticURL = tempVideo - - configStore.assign(dynamicWallpaper: wallpaper, toSpaceKey: spaceKey) + configStore.assign(dynamicWallpaper: staticWallpaper(tempVideo), toSpaceKey: spaceKey) configStore.remove(spaceKey: spaceKey) #expect(configStore.configs[spaceKey] == nil) @@ -73,15 +74,13 @@ struct ConfigStoreTests { // MARK: - Appearance Mode @Test func appearanceModeAssignment() async { - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") + let tempFile = makeJSONURL() defer { try? FileManager.default.removeItem(at: tempFile) } let configStore = ConfigStore(configFileURL: tempFile) let spaceKey = "test-display:file:///test/appearance.png" - let lightVideo = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_light.mp4") - let darkVideo = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_dark.mp4") - FileManager.default.createFile(atPath: lightVideo.path, contents: Data(), attributes: nil) - FileManager.default.createFile(atPath: darkVideo.path, contents: Data(), attributes: nil) + let lightVideo = makeVideo(suffix: "light") + let darkVideo = makeVideo(suffix: "dark") defer { try? FileManager.default.removeItem(at: lightVideo) try? FileManager.default.removeItem(at: darkVideo) @@ -104,27 +103,21 @@ struct ConfigStoreTests { // MARK: - Resolved URL @Test func resolvedURLStaticMode() async { - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") + let tempFile = makeJSONURL() defer { try? FileManager.default.removeItem(at: tempFile) } let configStore = ConfigStore(configFileURL: tempFile) - let spaceKey = "test-display:file:///test/resolved.png" - let tempVideo = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_resolved.mp4") - FileManager.default.createFile(atPath: tempVideo.path, contents: Data(), attributes: nil) + let tempVideo = makeVideo(suffix: "resolved") defer { try? FileManager.default.removeItem(at: tempVideo) } - var wallpaper = DynamicWallpaper() - wallpaper.mode = .staticVideo - wallpaper.staticURL = tempVideo - - configStore.assign(dynamicWallpaper: wallpaper, toSpaceKey: spaceKey) + configStore.assign(dynamicWallpaper: staticWallpaper(tempVideo), toSpaceKey: spaceKey) let resolved = configStore.resolvedURL(for: spaceKey, appearance: nil) #expect(resolved != nil) } @Test func resolvedURLMissingKey() async { - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") + let tempFile = makeJSONURL() defer { try? FileManager.default.removeItem(at: tempFile) } let configStore = ConfigStore(configFileURL: tempFile) @@ -136,16 +129,13 @@ struct ConfigStoreTests { /// displays that are unplugged, used to be deleted whenever the screen /// layout was re-read (every Space switch, config change, and launch). @Test func inactiveSpaceAssignmentsSurviveControllerSync() async { - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") + let tempFile = makeJSONURL() defer { try? FileManager.default.removeItem(at: tempFile) } let configStore = ConfigStore(configFileURL: tempFile) - - let tempVideo = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_inactive.mp4") - FileManager.default.createFile(atPath: tempVideo.path, contents: Data(), attributes: nil) + let tempVideo = makeVideo(suffix: "inactive") defer { try? FileManager.default.removeItem(at: tempVideo) } - var wallpaper = DynamicWallpaper() - wallpaper.staticURL = tempVideo + let wallpaper = staticWallpaper(tempVideo) let otherSpace = "999999:file:///Library/Desktop%20Pictures/Other%20Space.heic" let unpluggedDisplay = "424242:file:///Library/Desktop%20Pictures/External.heic" configStore.assign(dynamicWallpaper: wallpaper, toSpaceKey: otherSpace) @@ -165,16 +155,11 @@ struct ConfigStoreTests { try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) } - let tempFile = tempDir.appendingPathComponent("config_v2.json") - let configStore = ConfigStore(configFileURL: tempFile) - - let tempVideo = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_clear.mp4") - FileManager.default.createFile(atPath: tempVideo.path, contents: Data(), attributes: nil) + let configStore = ConfigStore(configFileURL: tempDir.appendingPathComponent("config_v2.json")) + let tempVideo = makeVideo(suffix: "clear") defer { try? FileManager.default.removeItem(at: tempVideo) } - var wallpaper = DynamicWallpaper() - wallpaper.staticURL = tempVideo - configStore.assign(dynamicWallpaper: wallpaper, toSpaceKey: "clear:test") + configStore.assign(dynamicWallpaper: staticWallpaper(tempVideo), toSpaceKey: "clear:test") configStore.clearAllData() #expect(configStore.configs.isEmpty) diff --git a/Tests/LivelyTests/DynamicWallpaperTests.swift b/Tests/LivelyTests/DynamicWallpaperTests.swift index d87398f..32807b3 100644 --- a/Tests/LivelyTests/DynamicWallpaperTests.swift +++ b/Tests/LivelyTests/DynamicWallpaperTests.swift @@ -11,6 +11,7 @@ struct DynamicWallpaperTests { wallpaper.mode = .staticVideo wallpaper.staticURL = URL(fileURLWithPath: "/tmp/static.mp4") #expect(wallpaper.url(for: nil) == wallpaper.staticURL) + #expect(wallpaper.url(for: NSAppearance(named: .darkAqua)) == wallpaper.staticURL) } // MARK: - Appearance Mode @@ -79,11 +80,4 @@ struct DynamicWallpaperTests { #expect(wallpaper.lightURL == nil) #expect(wallpaper.darkURL == nil) } - - @Test func nilAppearanceInStaticMode() { - var wallpaper = DynamicWallpaper() - wallpaper.mode = .staticVideo - wallpaper.staticURL = URL(fileURLWithPath: "/tmp/video.mp4") - #expect(wallpaper.url(for: nil) == wallpaper.staticURL) - } } diff --git a/Tests/LivelyTests/VideoValidationTests.swift b/Tests/LivelyTests/VideoValidationTests.swift index 5f89fef..62813c9 100644 --- a/Tests/LivelyTests/VideoValidationTests.swift +++ b/Tests/LivelyTests/VideoValidationTests.swift @@ -67,36 +67,6 @@ struct VideoValidationTests { #expect(VideoGravity.allCases.count == 2) } - // MARK: - ConfigStore Bookmark Integration - - @Test @MainActor func resolvedURLReturnsNilForMissingKey() async { - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") - defer { try? FileManager.default.removeItem(at: tempFile) } - - let store = ConfigStore(configFileURL: tempFile) - let result = store.resolvedURL(for: "nonexistent-key", appearance: nil) - #expect(result == nil) - } - - @Test @MainActor func resolvedURLReturnsValueForValidConfig() async { - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") - let tempVideo = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_resolved.mp4") - FileManager.default.createFile(atPath: tempVideo.path, contents: Data(), attributes: nil) - defer { - try? FileManager.default.removeItem(at: tempFile) - try? FileManager.default.removeItem(at: tempVideo) - } - - let store = ConfigStore(configFileURL: tempFile) - var wallpaper = DynamicWallpaper() - wallpaper.mode = .staticVideo - wallpaper.staticURL = tempVideo - - store.assign(dynamicWallpaper: wallpaper, toSpaceKey: "test-key") - let result = store.resolvedURL(for: "test-key", appearance: nil) - #expect(result != nil) - } - // MARK: - Backward Compatibility @Test func oldConfigWithoutNewFieldsDecodes() throws { From a120edee21eff4197e41cff69e28aeda8fe11fd6 Mon Sep 17 00:00:00 2001 From: harsha balakrishnan Date: Tue, 6 Oct 2026 13:40:56 +0530 Subject: [PATCH 4/5] refactor: collapse logger and preference boilerplate Share a single LivelyCategoryLogger path, seed UserDefaults defaults once, drop unused frameRateCap alias and power-source helper, and skip a redundant WallpaperWindow setFrame. --- Sources/Lively/Core/WallpaperController.swift | 17 ++++---- Sources/Lively/Core/WallpaperWindow.swift | 3 -- Sources/Lively/Logic/AppMetrics.swift | 18 ++++----- Sources/Lively/Logic/AppPreferences.swift | 24 +++-------- Sources/Lively/Logic/LivelyLogger.swift | 40 +++++++------------ 5 files changed, 35 insertions(+), 67 deletions(-) diff --git a/Sources/Lively/Core/WallpaperController.swift b/Sources/Lively/Core/WallpaperController.swift index 37d30fa..197a7a7 100644 --- a/Sources/Lively/Core/WallpaperController.swift +++ b/Sources/Lively/Core/WallpaperController.swift @@ -362,8 +362,10 @@ private final class WallpaperSessionManager { guard !isPaused else { return } for space in spaces { - var session = sessions[space.id] - if session == nil { + let session: WallpaperSession + if let existing = sessions[space.id] { + session = existing + } else { session = WallpaperSession(screen: space.screen) sessions[space.id] = session } @@ -372,7 +374,7 @@ private final class WallpaperSessionManager { let wallpaperConfig = configStore.configs[spaceKey]?.dynamicWallpaper ?? DynamicWallpaper() if let videoURL = bookmarkManager.urlForSpace(space, appearance: appearance) { - session?.play( + session.play( url: videoURL, wallpaper: wallpaperConfig, screen: space.screen, @@ -382,7 +384,7 @@ private final class WallpaperSessionManager { ) } else { bookmarkManager.stopScope(for: spaceKey) - session?.hide() + session.hide() } } } @@ -532,10 +534,7 @@ public final class WallpaperController: ObservableObject { } private func setupBatteryMonitor() { - // Lightweight poll — power-source CF notifications are awkward to bridge; - // 30s is enough for "pause on battery" without battery impact. - // Poll power source periodically (IOKit has no simple Combine publisher). - // 20s is responsive enough for "pause on battery" without busy-waiting. + // IOKit has no simple Combine publisher; 20s is enough for battery pause. powerSourceTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: true) { [weak self] _ in Task { @MainActor [weak self] in self?.refreshBatteryState() @@ -848,6 +847,4 @@ enum PowerSourceMonitor { return Snapshot(isOnBattery: onBattery, levelPercent: level) } - - static var isOnBattery: Bool { snapshot().isOnBattery } } diff --git a/Sources/Lively/Core/WallpaperWindow.swift b/Sources/Lively/Core/WallpaperWindow.swift index ff5821a..d51fb9e 100644 --- a/Sources/Lively/Core/WallpaperWindow.swift +++ b/Sources/Lively/Core/WallpaperWindow.swift @@ -47,9 +47,6 @@ class WallpaperWindow: NSWindow { self.canHide = false self.sharingType = .none self.animationBehavior = .none - - // Cover the full screen frame - setFrame(screen.frame, display: false) } // MARK: - Ordering diff --git a/Sources/Lively/Logic/AppMetrics.swift b/Sources/Lively/Logic/AppMetrics.swift index 615ff6e..093c3a4 100644 --- a/Sources/Lively/Logic/AppMetrics.swift +++ b/Sources/Lively/Logic/AppMetrics.swift @@ -1,20 +1,16 @@ import Foundation -// I-3: @MainActor matches actual usage — both call sites (ScreenCardView.onDrop -// and ConfigStore.assign) run on the main thread. This is safer than @unchecked -// Sendable, which promised thread-safety without a lock. @MainActor public final class AppMetrics { public static let shared = AppMetrics() - + private let defaults = UserDefaults.standard - + private let activatedKey = "wallpaper_applied_once" private let daysActiveKey = "days_with_wallpaper_active" private let lastActiveDateKey = "last_wallpaper_active_date" - - // I-1/I-2: Create DateFormatter once; en_US_POSIX + Gregorian ensures - // "yyyy-MM-dd" is stable across all device locales (Arabic, Hebrew, etc.). + + // en_US_POSIX + Gregorian keeps "yyyy-MM-dd" stable across locales. private let dayFormatter: DateFormatter = { let f = DateFormatter() f.dateFormat = "yyyy-MM-dd" @@ -26,11 +22,11 @@ public final class AppMetrics { private init() {} public var isActivated: Bool { - return defaults.bool(forKey: activatedKey) + defaults.bool(forKey: activatedKey) } - + public var daysWithWallpaperActive: Int { - return defaults.integer(forKey: daysActiveKey) + defaults.integer(forKey: daysActiveKey) } public func recordWallpaperApplied() { diff --git a/Sources/Lively/Logic/AppPreferences.swift b/Sources/Lively/Logic/AppPreferences.swift index 611e5c4..d71e3fe 100644 --- a/Sources/Lively/Logic/AppPreferences.swift +++ b/Sources/Lively/Logic/AppPreferences.swift @@ -119,8 +119,6 @@ public final class AppPreferences: ObservableObject { } } - public typealias FrameRateCap = MaxResolution - private enum Keys { static let startMinimized = "prefs.startMinimized" static let pauseOnBattery = "prefs.pauseOnBattery" @@ -205,20 +203,16 @@ public final class AppPreferences: ObservableObject { didSet { defaults.set(hasSeenSpacesCoach, forKey: Keys.hasSeenSpacesCoach) } } - public var frameRateCap: MaxResolution { - get { maxResolution } - set { maxResolution = newValue } - } - public init(defaults: UserDefaults = .standard) { self.defaults = defaults - if defaults.object(forKey: Keys.startMinimized) == nil { - defaults.set(true, forKey: Keys.startMinimized) - } - if defaults.object(forKey: Keys.pauseOnBattery) == nil { - defaults.set(true, forKey: Keys.pauseOnBattery) + func seedDefault(_ key: String, _ value: Any) { + if defaults.object(forKey: key) == nil { defaults.set(value, forKey: key) } } + seedDefault(Keys.startMinimized, true) + seedDefault(Keys.pauseOnBattery, true) + seedDefault(Keys.hardwareDecoding, true) + seedDefault(Keys.batteryPauseThreshold, 35.0) // The GitHub update check is opt-in (README privacy promise). Builds up // to 1.2.0 wrote `true` here as a default on first launch, which is // indistinguishable from an explicit opt-in, so reset it once. @@ -226,12 +220,6 @@ public final class AppPreferences: ObservableObject { defaults.set(false, forKey: Keys.checkForUpdates) defaults.set(true, forKey: Keys.updateCheckOptInMigrated) } - if defaults.object(forKey: Keys.hardwareDecoding) == nil { - defaults.set(true, forKey: Keys.hardwareDecoding) - } - if defaults.object(forKey: Keys.batteryPauseThreshold) == nil { - defaults.set(35.0, forKey: Keys.batteryPauseThreshold) - } self.startMinimized = defaults.bool(forKey: Keys.startMinimized) self.pauseOnBattery = defaults.bool(forKey: Keys.pauseOnBattery) diff --git a/Sources/Lively/Logic/LivelyLogger.swift b/Sources/Lively/Logic/LivelyLogger.swift index 43e9e26..7fc0a76 100644 --- a/Sources/Lively/Logic/LivelyLogger.swift +++ b/Sources/Lively/Logic/LivelyLogger.swift @@ -66,36 +66,26 @@ public final class LogStore: ObservableObject { public struct LivelyCategoryLogger: Sendable { let category: String private let osLogger: Logger - + init(category: String) { self.category = category self.osLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Lively", category: category) } - - public func info(_ message: String) { - osLogger.info("\(message, privacy: .public)") - if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { - Task { @MainActor in - LogStore.shared.add(message, level: .info, category: category) - } - } - } - - public func error(_ message: String) { - osLogger.error("\(message, privacy: .public)") - if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { - Task { @MainActor in - LogStore.shared.add(message, level: .error, category: category) - } + + public func info(_ message: String) { log(.info, message) } + public func error(_ message: String) { log(.error, message) } + public func debug(_ message: String) { log(.debug, message) } + + private func log(_ level: LogLevel, _ message: String) { + switch level { + case .info: osLogger.info("\(message, privacy: .public)") + case .warning: osLogger.warning("\(message, privacy: .public)") + case .error: osLogger.error("\(message, privacy: .public)") + case .debug: osLogger.debug("\(message, privacy: .public)") } - } - - public func debug(_ message: String) { - osLogger.debug("\(message, privacy: .public)") - if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { - Task { @MainActor in - LogStore.shared.add(message, level: .debug, category: category) - } + guard ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil else { return } + Task { @MainActor in + LogStore.shared.add(message, level: level, category: category) } } } From 0874fdd3eeb818aa8692bc2419b3f25650345749 Mon Sep 17 00:00:00 2001 From: harsha balakrishnan Date: Tue, 6 Oct 2026 14:47:35 +0530 Subject: [PATCH 5/5] test: Pin recordWallpaperApplied same-day increment as a no-op --- Tests/LivelyTests/AppMetricsTests.swift | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Tests/LivelyTests/AppMetricsTests.swift diff --git a/Tests/LivelyTests/AppMetricsTests.swift b/Tests/LivelyTests/AppMetricsTests.swift new file mode 100644 index 0000000..201254f --- /dev/null +++ b/Tests/LivelyTests/AppMetricsTests.swift @@ -0,0 +1,41 @@ +import Testing +@testable import LivelyCore +import Foundation + +@MainActor +struct AppMetricsTests { + private let keys = [ + "wallpaper_applied_once", + "days_with_wallpaper_active", + "last_wallpaper_active_date", + ] + + private func withCleanMetrics(_ body: () -> Void) { + let d = UserDefaults.standard + let saved: [(String, Any?)] = keys.map { ($0, d.object(forKey: $0)) } + defer { + for (key, value) in saved { + if let value { + d.set(value, forKey: key) + } else { + d.removeObject(forKey: key) + } + } + } + for key in keys { d.removeObject(forKey: key) } + body() + } + + @Test func recordWallpaperAppliedCountsOncePerDay() { + withCleanMetrics { + let metrics = AppMetrics.shared + #expect(metrics.daysWithWallpaperActive == 0) + metrics.recordWallpaperApplied() + let afterFirst = metrics.daysWithWallpaperActive + #expect(afterFirst == 1) + #expect(metrics.isActivated) + metrics.recordWallpaperApplied() + #expect(metrics.daysWithWallpaperActive == afterFirst) + } + } +}