@@ -14,6 +14,9 @@ import com.sameerasw.airsync.domain.model.NetworkDeviceConnection
1414import com.sameerasw.airsync.domain.model.NotificationApp
1515import kotlinx.coroutines.flow.Flow
1616import kotlinx.coroutines.flow.map
17+ import kotlinx.coroutines.runBlocking
18+ import kotlinx.coroutines.flow.first
19+ import org.json.JSONObject
1720
1821val Context .dataStore: DataStore <Preferences > by preferencesDataStore(name = " airsync_settings" )
1922
@@ -54,7 +57,9 @@ class DataStoreManager(private val context: Context) {
5457 private val SEND_NOW_PLAYING_ENABLED = booleanPreferencesKey(" send_now_playing_enabled" )
5558 // New: Keep previous link toggle
5659 private val KEEP_PREVIOUS_LINK_ENABLED = booleanPreferencesKey(" keep_previous_link_enabled" )
57- private val TAILSCALE_SUPPORT_ENABLED = booleanPreferencesKey(" tailscale_support_enabled" )
60+ // New: Always show in Smartspacer toggle
61+ private val SMARTSPACER_SHOW_WHEN_DISCONNECTED = booleanPreferencesKey(" smartspacer_show_when_disconnected" )
62+ private val EXPAND_NETWORKING_ENABLED = booleanPreferencesKey(" expand_networking_enabled" )
5863
5964 // Network-aware device connections
6065 private val NETWORK_DEVICES_PREFIX = " network_device_"
@@ -197,15 +202,28 @@ class DataStoreManager(private val context: Context) {
197202 }
198203 }
199204
200- suspend fun setTailscaleSupportEnabled (enabled : Boolean ) {
205+ // New: Always show in Smartspacer toggle
206+ suspend fun setSmartspacerShowWhenDisconnected (enabled : Boolean ) {
207+ context.dataStore.edit { preferences ->
208+ preferences[SMARTSPACER_SHOW_WHEN_DISCONNECTED ] = enabled
209+ }
210+ }
211+
212+ fun getSmartspacerShowWhenDisconnected (): Flow <Boolean > {
213+ return context.dataStore.data.map { preferences ->
214+ preferences[SMARTSPACER_SHOW_WHEN_DISCONNECTED ] ? : false
215+ }
216+ }
217+
218+ suspend fun setExpandNetworkingEnabled (enabled : Boolean ) {
201219 context.dataStore.edit { prefs ->
202- prefs[TAILSCALE_SUPPORT_ENABLED ] = enabled
220+ prefs[EXPAND_NETWORKING_ENABLED ] = enabled
203221 }
204222 }
205223
206- fun getTailscaleSupportEnabled (): Flow <Boolean > {
224+ fun getExpandNetworkingEnabled (): Flow <Boolean > {
207225 return context.dataStore.data.map { prefs ->
208- prefs[TAILSCALE_SUPPORT_ENABLED ] ? : false
226+ prefs[EXPAND_NETWORKING_ENABLED ] ? : false
209227 }
210228 }
211229
@@ -543,4 +561,146 @@ class DataStoreManager(private val context: Context) {
543561 preferences[stringPreferencesKey(" ${NETWORK_DEVICES_PREFIX }${deviceName} _last_connected" )] = timestamp.toString()
544562 }
545563 }
546- }
564+
565+ /* *
566+ * Export all DataStore preferences to a JSON string.
567+ * Excludes very large strings and anything that looks like a base64 image (data:image/...base64,...)
568+ */
569+ suspend fun exportAllDataToJson (): String {
570+ val prefs = context.dataStore.data.first()
571+ val exportObj = JSONObject ()
572+ val dataObj = JSONObject ()
573+ val mutedArray = org.json.JSONArray ()
574+
575+ prefs.asMap().forEach { (key, value) ->
576+ try {
577+ // Skip nulls
578+ if (value == null ) return @forEach
579+
580+ // If string looks like an embedded image or is very large, skip
581+ if (value is String ) {
582+ val lower = value.lowercase()
583+ if (lower.contains(" data:image" ) || lower.contains(" base64," ) || value.length > 10000 ) {
584+ // mark skipped
585+ return @forEach
586+ }
587+ }
588+
589+ // Collect muted apps: keys like app_<package>_enabled with value false
590+ if (key.name.startsWith(" app_" ) && key.name.endsWith(" _enabled" )) {
591+ val pkg = key.name.removePrefix(" app_" ).removeSuffix(" _enabled" )
592+ val enabled = (value as ? Boolean ) ? : true
593+ if (! enabled) {
594+ mutedArray.put(pkg)
595+ }
596+ }
597+
598+ val entry = JSONObject ()
599+ when (value) {
600+ is String -> {
601+ entry.put(" type" , " string" )
602+ entry.put(" value" , value)
603+ }
604+ is Boolean -> {
605+ entry.put(" type" , " boolean" )
606+ entry.put(" value" , value)
607+ }
608+ is Int -> {
609+ entry.put(" type" , " int" )
610+ entry.put(" value" , value)
611+ }
612+ is Long -> {
613+ entry.put(" type" , " long" )
614+ entry.put(" value" , value)
615+ }
616+ else -> {
617+ // Fallback to string representation
618+ entry.put(" type" , " string" )
619+ entry.put(" value" , value.toString())
620+ }
621+ }
622+ dataObj.put(key.name, entry)
623+ } catch (_: Exception ) {
624+ // ignore problematic entries
625+ }
626+ }
627+
628+ exportObj.put(" version" , 1 )
629+ exportObj.put(" preferences" , dataObj)
630+ // Include list of apps explicitly disabled for notification syncing so import can restore this
631+ exportObj.put(" muted_apps" , mutedArray)
632+ return exportObj.toString()
633+ }
634+
635+ /* *
636+ * Import preferences from JSON produced by exportAllDataToJson.
637+ * Only writes keys present in the JSON; missing keys are left unchanged.
638+ */
639+ suspend fun importAllDataFromJson (json : String ): Boolean {
640+ try {
641+ val root = JSONObject (json)
642+ val prefsObj = root.optJSONObject(" preferences" ) ? : return false
643+ val mutedApps = mutableListOf<String >()
644+ val mutedJson = root.optJSONArray(" muted_apps" )
645+ if (mutedJson != null ) {
646+ for (i in 0 until mutedJson.length()) {
647+ try { mutedApps.add(mutedJson.getString(i)) } catch (_: Exception ) {}
648+ }
649+ }
650+
651+ context.dataStore.edit { preferences ->
652+ val keys = prefsObj.keys()
653+ while (keys.hasNext()) {
654+ val keyName = keys.next()
655+ try {
656+ val entry = prefsObj.getJSONObject(keyName)
657+ val type = entry.optString(" type" , " string" )
658+ when (type) {
659+ " boolean" -> {
660+ val key = booleanPreferencesKey(keyName)
661+ val v = entry.getBoolean(" value" )
662+ preferences[key] = v
663+ }
664+ " int" -> {
665+ val key = intPreferencesKey(keyName)
666+ val v = entry.getInt(" value" )
667+ preferences[key] = v
668+ }
669+ " long" -> {
670+ val key = longPreferencesKey(keyName)
671+ val v = entry.getLong(" value" )
672+ preferences[key] = v
673+ }
674+ else -> {
675+ val key = stringPreferencesKey(keyName)
676+ val v = entry.optString(" value" , " " )
677+ // Skip if value looks like embedded image or very large
678+ val lower = v.lowercase()
679+ if (lower.contains(" data:image" ) || lower.contains(" base64," ) || v.length > 10000 ) {
680+ // skip this key
681+ } else {
682+ preferences[key] = v
683+ }
684+ }
685+ }
686+ } catch (_: Exception ) {
687+ // ignore specific key import errors
688+ }
689+ }
690+
691+ // Apply muted apps (explicitly disable per-app notification flags)
692+ mutedApps.forEach { pkg ->
693+ try {
694+ val enabledKey = booleanPreferencesKey(" app_${pkg} _enabled" )
695+ preferences[enabledKey] = false
696+ } catch (_: Exception ) {}
697+ }
698+ }
699+
700+ return true
701+ } catch (e: Exception ) {
702+ e.printStackTrace()
703+ return false
704+ }
705+ }
706+ }
0 commit comments