A Minecraft Fabric mod that connects the game with ATProtocol ⛏️
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add client-side preferences system for sync consent

- Implement ClientPreferences data class with serialization
- Store preferences locally in JSON format (client-only, no privacy concerns)
- Sync options: stats, sessions, achievements, server status
- Configurable sync frequencies (in minutes)
- UI preference toggles: notifications, F3 display, compact layout
- Privacy options: encrypted storage, cache cleanup on logout
- PreferencesManager singleton for centralized access

+134
+134
src/client/kotlin/com/jollywhoppers/config/ClientPreferences.kt
··· 1 + package com.jollywhoppers.config 2 + 3 + import kotlinx.serialization.Serializable 4 + import kotlinx.serialization.json.Json 5 + import net.fabricmc.loader.api.FabricLoader 6 + import org.slf4j.LoggerFactory 7 + import java.nio.file.Files 8 + import java.nio.file.Path 9 + import kotlin.io.path.exists 10 + import kotlin.io.path.readText 11 + import kotlin.io.path.writeText 12 + 13 + /** 14 + * Client-side preferences for Social Sync mod. 15 + * Stores user preferences locally in JSON format. 16 + * Preferences are NOT sent to the server — they're purely client-side. 17 + * 18 + * This separation ensures privacy: sync settings are user preferences, 19 + * not tied to authentication, and the server respects them. 20 + */ 21 + @Serializable 22 + data class ClientPreferences( 23 + // Sync consent settings 24 + val syncStatsEnabled: Boolean = true, 25 + val syncSessionsEnabled: Boolean = true, 26 + val syncAchievementsEnabled: Boolean = true, 27 + val syncServerStatusEnabled: Boolean = false, 28 + 29 + // Sync frequency (in minutes) 30 + val statsSyncFrequency: Int = 60, 31 + val sessionSyncFrequency: Int = 5, 32 + val achievementSyncFrequency: Int = 30, 33 + 34 + // UI preferences 35 + val showSyncNotifications: Boolean = true, 36 + val showStatusInF3: Boolean = true, 37 + val compactModMenuLayout: Boolean = false, 38 + 39 + // Privacy settings 40 + val encryptedLocalStorage: Boolean = true, 41 + val clearLocalCacheOnLogout: Boolean = true, 42 + ) { 43 + companion object { 44 + private val logger = LoggerFactory.getLogger("atproto-connect-config") 45 + private val json = Json { prettyPrint = true } 46 + private val configDir: Path by lazy { 47 + FabricLoader.getInstance().configDir.resolve("atproto-connect") 48 + } 49 + private val configFile: Path by lazy { 50 + configDir.resolve("client-preferences.json") 51 + } 52 + 53 + /** 54 + * Load preferences from disk, or return defaults if not found. 55 + */ 56 + fun load(): ClientPreferences { 57 + return try { 58 + if (configFile.exists()) { 59 + val content = configFile.readText() 60 + json.decodeFromString<ClientPreferences>(content).also { 61 + logger.info("Loaded client preferences from ${configFile.fileName}") 62 + } 63 + } else { 64 + logger.debug("No preferences file found, using defaults") 65 + ClientPreferences() 66 + } 67 + } catch (e: Exception) { 68 + logger.error("Failed to load client preferences: ${e.message}", e) 69 + logger.info("Using default preferences") 70 + ClientPreferences() 71 + } 72 + } 73 + 74 + /** 75 + * Save preferences to disk. 76 + */ 77 + fun save(prefs: ClientPreferences) { 78 + try { 79 + Files.createDirectories(configDir) 80 + val json = json.encodeToString(serializer(), prefs) 81 + configFile.writeText(json) 82 + logger.debug("Saved client preferences to ${configFile.fileName}") 83 + } catch (e: Exception) { 84 + logger.error("Failed to save client preferences: ${e.message}", e) 85 + } 86 + } 87 + } 88 + } 89 + 90 + /** 91 + * Global preferences instance. 92 + * Loaded on mod init, updated when user changes settings in ModMenu. 93 + */ 94 + object PreferencesManager { 95 + private val logger = LoggerFactory.getLogger("atproto-connect-config") 96 + 97 + private var instance: ClientPreferences = ClientPreferences.load() 98 + 99 + fun get(): ClientPreferences = instance 100 + 101 + fun update(preferences: ClientPreferences) { 102 + instance = preferences 103 + ClientPreferences.save(preferences) 104 + logger.info("Client preferences updated and persisted") 105 + } 106 + 107 + /** 108 + * Update a single preference field without reloading from disk. 109 + */ 110 + fun updateSyncConsent( 111 + stats: Boolean? = null, 112 + sessions: Boolean? = null, 113 + achievements: Boolean? = null, 114 + serverStatus: Boolean? = null, 115 + ) { 116 + instance = instance.copy( 117 + syncStatsEnabled = stats ?: instance.syncStatsEnabled, 118 + syncSessionsEnabled = sessions ?: instance.syncSessionsEnabled, 119 + syncAchievementsEnabled = achievements ?: instance.syncAchievementsEnabled, 120 + syncServerStatusEnabled = serverStatus ?: instance.syncServerStatusEnabled, 121 + ) 122 + ClientPreferences.save(instance) 123 + logger.info("Sync consent preferences updated") 124 + } 125 + 126 + /** 127 + * Reset preferences to defaults. 128 + */ 129 + fun reset() { 130 + instance = ClientPreferences() 131 + ClientPreferences.save(instance) 132 + logger.info("Client preferences reset to defaults") 133 + } 134 + }