mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
Sync sidebar/tab bar/home layout per-platform via iCloud
Extends `SettingsKey.isPlatformSpecific` to cover home, tab bar, and sidebar layout, plus player details panel and video swipe actions, so iOS devices sync these with other iOS devices (and tvOS with tvOS, macOS with macOS) instead of overwriting each other via the shared iCloud key. Adds a one-shot migration that copies legacy unprefixed values into the new platform-prefixed slots locally and in iCloud, preserving protected-key timestamps.
This commit is contained in:
@@ -116,9 +116,26 @@ enum SettingsKey: String, CaseIterable {
|
||||
case onboardingCompleted
|
||||
|
||||
/// Whether this key should have platform-specific prefixes.
|
||||
/// Platform-specific keys are stored under a `iOS.` / `macOS.` / `tvOS.` prefix
|
||||
/// in both UserDefaults and iCloud, so each platform family syncs independently.
|
||||
var isPlatformSpecific: Bool {
|
||||
switch self {
|
||||
case .preferredQuality, .cellularQuality, .macPlayerMode, .listStyle:
|
||||
case .preferredQuality, .cellularQuality, .macPlayerMode, .listStyle,
|
||||
// Home layout — different UI paradigms per platform
|
||||
.homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout,
|
||||
.homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit,
|
||||
// Tab bar (compact size class) layout
|
||||
.tabBarItemOrder, .tabBarItemVisibility, .tabBarStartupTab,
|
||||
// Sidebar layout/selection
|
||||
.sidebarMainItemOrder, .sidebarMainItemVisibility, .sidebarStartupTab,
|
||||
.sidebarSourcesEnabled, .sidebarSourceSort, .sidebarSourcesLimitEnabled, .sidebarMaxSources,
|
||||
.sidebarChannelsEnabled, .sidebarMaxChannels, .sidebarChannelSort, .sidebarChannelsLimitEnabled,
|
||||
.sidebarPlaylistsEnabled, .sidebarMaxPlaylists, .sidebarPlaylistSort, .sidebarPlaylistsLimitEnabled,
|
||||
// Player details panel — iOS/iPadOS only, different on other platforms
|
||||
.floatingDetailsPanelSide, .floatingDetailsPanelWidth,
|
||||
.landscapeDetailsPanelVisible, .landscapeDetailsPanelPinned,
|
||||
// Video swipe actions — touch-gesture feature
|
||||
.videoSwipeActionOrder, .videoSwipeActionVisibility:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
74
Yattee/Core/Settings/SettingsManager+Migration.swift
Normal file
74
Yattee/Core/Settings/SettingsManager+Migration.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// SettingsManager+Migration.swift
|
||||
// Yattee
|
||||
//
|
||||
// One-shot migrations that move legacy unprefixed values under
|
||||
// platform-specific keys when `SettingsKey.isPlatformSpecific` flips to true.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension SettingsManager {
|
||||
private static let migrationFlagKey = "didMigratePlatformSpecificLayoutKeys_v1"
|
||||
|
||||
// Mirrors `SettingsManager+CloudSync.protectedVisibilityKeys`. Kept in sync manually
|
||||
// because that collection is fileprivate; the set is small and rarely changes.
|
||||
private static let protectedKeysForMigration: Set<SettingsKey> = [
|
||||
.homeShortcutVisibility,
|
||||
.homeSectionVisibility,
|
||||
.homeShortcutOrder,
|
||||
.homeSectionOrder
|
||||
]
|
||||
|
||||
/// Copies any legacy unprefixed values for keys that became platform-specific into their
|
||||
/// new `iOS.` / `macOS.` / `tvOS.` slots, both locally and (if iCloud sync is on) in iCloud.
|
||||
/// Leaves the legacy unprefixed keys in place so older builds on other devices still work.
|
||||
func migrateLayoutKeysToPlatformPrefixed() {
|
||||
guard !localDefaults.bool(forKey: Self.migrationFlagKey) else { return }
|
||||
|
||||
let keysNeedingMigration = SettingsKey.allCases.filter { $0.isPlatformSpecific }
|
||||
let pushToCloud = iCloudSyncEnabled && syncSettings
|
||||
|
||||
for key in keysNeedingMigration {
|
||||
let pKey = platformKey(key)
|
||||
let legacyKey = key.rawValue
|
||||
|
||||
// Skip if the prefixed form already exists or if the legacy key is the same as the prefixed
|
||||
// key (e.g. on a platform where `platformKey` didn't rewrite it, though that shouldn't happen
|
||||
// for isPlatformSpecific keys).
|
||||
guard pKey != legacyKey,
|
||||
localDefaults.object(forKey: pKey) == nil,
|
||||
let legacyValue = localDefaults.object(forKey: legacyKey)
|
||||
else { continue }
|
||||
|
||||
localDefaults.set(legacyValue, forKey: pKey)
|
||||
|
||||
if Self.protectedKeysForMigration.contains(key) {
|
||||
let legacyTimestampKey = "\(legacyKey)_modifiedAt"
|
||||
let newTimestampKey = modifiedAtKey(for: key)
|
||||
let legacyTimestamp = localDefaults.double(forKey: legacyTimestampKey)
|
||||
if legacyTimestamp > 0, localDefaults.double(forKey: newTimestampKey) == 0 {
|
||||
localDefaults.set(legacyTimestamp, forKey: newTimestampKey)
|
||||
}
|
||||
}
|
||||
|
||||
if pushToCloud {
|
||||
ubiquitousStore.set(legacyValue, forKey: pKey)
|
||||
if Self.protectedKeysForMigration.contains(key) {
|
||||
let newTimestampKey = modifiedAtKey(for: key)
|
||||
let timestamp = localDefaults.double(forKey: newTimestampKey)
|
||||
if timestamp > 0 {
|
||||
ubiquitousStore.set(timestamp, forKey: newTimestampKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pushToCloud {
|
||||
ubiquitousStore.synchronize()
|
||||
}
|
||||
|
||||
localDefaults.set(true, forKey: Self.migrationFlagKey)
|
||||
LoggingService.shared.logCloudKit("Migrated legacy layout keys to platform-prefixed storage")
|
||||
}
|
||||
}
|
||||
@@ -252,6 +252,12 @@ final class SettingsManager {
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot migration: move legacy unprefixed values for keys that became
|
||||
// platform-specific into their new iOS./macOS./tvOS. slots. Must run before
|
||||
// the initial iCloud refresh so it can seed the prefixed iCloud slot from the
|
||||
// current local value before any remote data is read.
|
||||
migrateLayoutKeysToPlatformPrefixed()
|
||||
|
||||
// Initial sync from iCloud to local storage (async to avoid blocking app launch)
|
||||
// This ensures local defaults have the latest iCloud values before any reads.
|
||||
// While sync is pending, suppress iCloud writes from set() to prevent stale
|
||||
|
||||
Reference in New Issue
Block a user