diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/Coding.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/Coding.swift index ed06c64e..10094bcd 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/Coding.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/Coding.swift @@ -13,6 +13,9 @@ private protocol PreferenceProtocol { init() } +extension Preference: PreferenceProtocol { +} + struct PreferenceCoding: Codable { let wrapped: Wrapped diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/Legacy/PreferenceStore+Migrate.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/Legacy/PreferenceStore+Migrate.swift index 1858e2b8..43d87a71 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/Legacy/PreferenceStore+Migrate.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/Legacy/PreferenceStore+Migrate.swift @@ -6,67 +6,97 @@ // import Foundation +import UIKit extension PreferenceStore { func migrate(from legacy: LegacyPreferences) { - self.theme = switch legacy.theme { - case .light: .light - case .dark: .dark - default: .unspecified + let migrations: [any MigrationProtocol] = [ + Migration(from: \.theme.theme, to: \.$theme), + Migration(from: \.pureBlackDarkMode, to: \.$pureBlackDarkMode), + Migration(from: \.accentColor, to: \.$accentColor), + Migration(from: \.avatarStyle, to: \.$avatarStyle), + Migration(from: \.hideCustomEmojiInUsernames, to: \.$hideCustomEmojiInUsernames), + Migration(from: \.showIsStatusReplyIcon, to: \.$showIsStatusReplyIcon), + Migration(from: \.alwaysShowStatusVisibilityIcon, to: \.$alwaysShowStatusVisibilityIcon), + Migration(from: \.hideActionsInTimeline, to: \.$hideActionsInTimeline), + Migration(from: \.showLinkPreviews, to: \.$showLinkPreviews), + Migration(from: \.leadingStatusSwipeActions, to: \.$leadingStatusSwipeActions), + Migration(from: \.trailingStatusSwipeActions, to: \.$trailingStatusSwipeActions), + Migration(from: \.widescreenNavigationMode, to: \.$widescreenNavigationMode), + Migration(from: \.underlineTextLinks, to: \.$underlineTextLinks), + Migration(from: \.showAttachmentsInTimeline, to: \.$showAttachmentsInTimeline), + + Migration(from: \.defaultPostVisibility, to: \.$defaultPostVisibility), + Migration(from: \.defaultReplyVisibility, to: \.$defaultReplyVisibility), + Migration(from: \.requireAttachmentDescriptions, to: \.$requireAttachmentDescriptions), + Migration(from: \.contentWarningCopyMode, to: \.$contentWarningCopyMode), + Migration(from: \.mentionReblogger, to: \.$mentionReblogger), + Migration(from: \.useTwitterKeyboard, to: \.$useTwitterKeyboard), + + Migration(from: \.attachmentBlurMode, to: \.$attachmentBlurMode), + Migration(from: \.blurMediaBehindContentWarning, to: \.$blurMediaBehindContentWarning), + Migration(from: \.automaticallyPlayGifs, to: \.$automaticallyPlayGifs), + Migration(from: \.showUncroppedMediaInline, to: \.$showUncroppedMediaInline), + Migration(from: \.showAttachmentBadges, to: \.$showAttachmentBadges), + Migration(from: \.attachmentAltBadgeInverted, to: \.$attachmentAltBadgeInverted), + + Migration(from: \.openLinksInApps, to: \.$openLinksInApps), + Migration(from: \.useInAppSafari, to: \.$useInAppSafari), + Migration(from: \.inAppSafariAutomaticReaderMode, to: \.$inAppSafariAutomaticReaderMode), + Migration(from: \.expandAllContentWarnings, to: \.$expandAllContentWarnings), + Migration(from: \.collapseLongPosts, to: \.$collapseLongPosts), + Migration(from: \.oppositeCollapseKeywords, to: \.$oppositeCollapseKeywords), + Migration(from: \.confirmBeforeReblog, to: \.$confirmBeforeReblog), + Migration(from: \.timelineStateRestoration, to: \.$timelineStateRestoration), + Migration(from: \.timelineSyncMode, to: \.$timelineSyncMode), + Migration(from: \.hideReblogsInTimelines, to: \.$hideReblogsInTimelines), + Migration(from: \.hideRepliesInTimelines, to: \.$hideRepliesInTimelines), + + Migration(from: \.showFavoriteAndReblogCounts, to: \.$showFavoriteAndReblogCounts), + Migration(from: \.defaultNotificationsMode, to: \.$defaultNotificationsMode), + Migration(from: \.grayscaleImages, to: \.$grayscaleImages), + Migration(from: \.disableInfiniteScrolling, to: \.$disableInfiniteScrolling), + Migration(from: \.hideTrends, to: \.$hideTrends), + + Migration(from: \.statusContentType, to: \.$statusContentType), + Migration(from: \.reportErrorsAutomatically, to: \.$reportErrorsAutomatically), + Migration(from: \.enabledFeatureFlags, to: \.$enabledFeatureFlags), + + Migration(from: \.hasShownLocalTimelineDescription, to: \.$hasShownLocalTimelineDescription), + Migration(from: \.hasShownFederatedTimelineDescription, to: \.$hasShownFederatedTimelineDescription), + ] + + for migration in migrations { + migration.migrate(from: legacy, to: self) + } + } +} + +private protocol MigrationProtocol { + func migrate(from legacy: LegacyPreferences, to store: PreferenceStore) +} + +private struct Migration: MigrationProtocol where Key.Value: Equatable { + let from: KeyPath + let to: KeyPath> + + func migrate(from legacy: LegacyPreferences, to store: PreferenceStore) { + let value = legacy[keyPath: from] + if value != Key.defaultValue { + Preference.set(enclosingInstance: store, storage: to.appending(path: \.preference), newValue: value) + } + } +} + +private extension UIUserInterfaceStyle { + var theme: Theme { + switch self { + case .light: + .light + case .dark: + .dark + default: + .unspecified } - self.pureBlackDarkMode = legacy.pureBlackDarkMode - self.accentColor = legacy.accentColor - self.avatarStyle = legacy.avatarStyle - self.hideCustomEmojiInUsernames = legacy.hideCustomEmojiInUsernames - self.showIsStatusReplyIcon = legacy.showIsStatusReplyIcon - self.alwaysShowStatusVisibilityIcon = legacy.alwaysShowStatusVisibilityIcon - self.hideActionsInTimeline = legacy.hideActionsInTimeline - self.showLinkPreviews = legacy.showLinkPreviews - self.leadingStatusSwipeActions = legacy.leadingStatusSwipeActions - self.trailingStatusSwipeActions = legacy.trailingStatusSwipeActions - if legacy.widescreenNavigationMode != .splitScreen { - self.widescreenNavigationMode = legacy.widescreenNavigationMode - } - self.underlineTextLinks = legacy.underlineTextLinks - self.showAttachmentsInTimeline = legacy.showAttachmentsInTimeline - - self.defaultPostVisibility = legacy.defaultPostVisibility - self.defaultReplyVisibility = legacy.defaultReplyVisibility - self.requireAttachmentDescriptions = legacy.requireAttachmentDescriptions - self.contentWarningCopyMode = legacy.contentWarningCopyMode - self.mentionReblogger = legacy.mentionReblogger - self.useTwitterKeyboard = legacy.useTwitterKeyboard - - self.attachmentBlurMode = legacy.attachmentBlurMode - self.blurMediaBehindContentWarning = legacy.blurMediaBehindContentWarning - self.automaticallyPlayGifs = legacy.automaticallyPlayGifs - self.showUncroppedMediaInline = legacy.showUncroppedMediaInline - self.showAttachmentBadges = legacy.showAttachmentBadges - self.attachmentAltBadgeInverted = legacy.attachmentAltBadgeInverted - - self.openLinksInApps = legacy.openLinksInApps - self.useInAppSafari = legacy.useInAppSafari - self.inAppSafariAutomaticReaderMode = legacy.inAppSafariAutomaticReaderMode - self.expandAllContentWarnings = legacy.expandAllContentWarnings - self.collapseLongPosts = legacy.collapseLongPosts - self.oppositeCollapseKeywords = legacy.oppositeCollapseKeywords - self.confirmBeforeReblog = legacy.confirmBeforeReblog - self.timelineStateRestoration = legacy.timelineStateRestoration - self.timelineSyncMode = legacy.timelineSyncMode - self.hideReblogsInTimelines = legacy.hideReblogsInTimelines - self.hideRepliesInTimelines = legacy.hideRepliesInTimelines - - self.showFavoriteAndReblogCounts = legacy.showFavoriteAndReblogCounts - self.defaultNotificationsMode = legacy.defaultNotificationsMode - self.grayscaleImages = legacy.grayscaleImages - self.disableInfiniteScrolling = legacy.disableInfiniteScrolling - self.hideTrends = legacy.hideTrends - - self.statusContentType = legacy.statusContentType - self.reportErrorsAutomatically = legacy.reportErrorsAutomatically - self.enabledFeatureFlags = legacy.enabledFeatureFlags - - self.hasShownLocalTimelineDescription = legacy.hasShownLocalTimelineDescription - self.hasShownFederatedTimelineDescription = legacy.hasShownFederatedTimelineDescription } } diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/Preference.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/Preference.swift index d15a6c54..9d96aec8 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/Preference.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/Preference.swift @@ -45,10 +45,10 @@ final class Preference: Codable { storage storageKeyPath: ReferenceWritableKeyPath ) -> Key.Value { get { - get(enclosingInstance: instance, wrapped: wrappedKeyPath, storage: storageKeyPath) + get(enclosingInstance: instance, storage: storageKeyPath) } set { - set(enclosingInstance: instance, wrapped: wrappedKeyPath, storage: storageKeyPath, newValue: newValue) + set(enclosingInstance: instance, storage: storageKeyPath, newValue: newValue) Key.didSet(in: instance, newValue: newValue) } } @@ -57,8 +57,7 @@ final class Preference: Codable { @inline(__always) static func get( enclosingInstance: Enclosing, - wrapped: ReferenceWritableKeyPath, - storage: ReferenceWritableKeyPath + storage: KeyPath ) -> Key.Value where Enclosing: ObservableObject, Enclosing.ObjectWillChangePublisher == ObservableObjectPublisher { let pref = enclosingInstance[keyPath: storage] return pref.storedValue ?? Key.defaultValue @@ -68,16 +67,26 @@ final class Preference: Codable { @inline(__always) static func set( enclosingInstance: Enclosing, - wrapped: ReferenceWritableKeyPath, - storage: ReferenceWritableKeyPath, + storage: KeyPath, newValue: Key.Value ) where Enclosing: ObservableObject, Enclosing.ObjectWillChangePublisher == ObservableObjectPublisher { enclosingInstance.objectWillChange.send() let pref = enclosingInstance[keyPath: storage] pref.storedValue = newValue } - - var projectedValue: some Publisher { - $storedValue.map { $0 ?? Key.defaultValue } + + var projectedValue: PreferencePublisher { + .init(preference: self) + } +} + +struct PreferencePublisher: Publisher { + typealias Output = Key.Value + typealias Failure = Never + + let preference: Preference + + func receive(subscriber: S) where S : Subscriber, Never == S.Failure, Key.Value == S.Input { + preference.$storedValue.map { $0 ?? Key.defaultValue }.receive(subscriber: subscriber) } } diff --git a/Packages/TuskerPreferences/Tests/TuskerPreferencesTests/PreferenceStoreTests.swift b/Packages/TuskerPreferences/Tests/TuskerPreferencesTests/PreferenceStoreTests.swift index 8ee6f19f..1527dbec 100644 --- a/Packages/TuskerPreferences/Tests/TuskerPreferencesTests/PreferenceStoreTests.swift +++ b/Packages/TuskerPreferences/Tests/TuskerPreferencesTests/PreferenceStoreTests.swift @@ -21,10 +21,10 @@ final class PreferenceStoreTests: XCTestCase { // the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually var test: Bool { get { - Preference.get(enclosingInstance: self, wrapped: \.test, storage: \._test) + Preference.get(enclosingInstance: self, storage: \._test) } set { - Preference.set(enclosingInstance: self, wrapped: \.test, storage: \._test, newValue: newValue) + Preference.set(enclosingInstance: self, storage: \._test, newValue: newValue) } }