diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index 78a6fcd112..114e9c5ee7 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -56,6 +56,7 @@ import com.halilibo.richtext.ui.material3.RichText import com.halilibo.richtext.ui.resolveDefaults import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok import com.vitorpamplona.amethyst.commons.resources.push_server_explainer @@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.commons.ui.components.SpinnerSelectionDialog import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsBlockTile import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 80e1666fc1..781ac1b91d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -28,6 +28,7 @@ import androidx.security.crypto.EncryptedSharedPreferences import coil3.disk.DiskCache import coil3.memory.MemoryCache import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.UiSettings import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.BitcoinExplorerEndpoint import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.IncomingOtsEventVerifier @@ -65,7 +66,6 @@ import com.vitorpamplona.amethyst.commons.tor.TorSettings import com.vitorpamplona.amethyst.connectedApps.DataStoreNostrSignerPermissionStore import com.vitorpamplona.amethyst.connectedApps.nip46.DataStoreNip46ClientStore import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.UiSettings import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever import com.vitorpamplona.amethyst.model.preferences.DrawerSectionCollapsePreferences diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 8c514f3774..430f20be37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -26,6 +26,7 @@ import android.content.SharedPreferences import androidx.compose.runtime.Immutable import androidx.core.content.edit import com.vitorpamplona.amethyst.commons.model.HomeFeedType +import com.vitorpamplona.amethyst.commons.model.UiSettings import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType import com.vitorpamplona.amethyst.commons.model.clink.ClinkDebitWalletEntry import com.vitorpamplona.amethyst.commons.model.concord.ConcordViewMode @@ -59,7 +60,6 @@ import com.vitorpamplona.amethyst.commons.model.preferences.readLegacyAccountSec import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.UiSettings import com.vitorpamplona.amethyst.model.backups.BackupConflictStorage import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt index e4029c8627..cd6fb36b17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt @@ -27,8 +27,8 @@ import android.os.Bundle import android.widget.Toast import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.napplet.NappletLauncher import com.vitorpamplona.amethyst.napplet.NappletWebViewProfiles import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt index 4148227f82..fe4df20db7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -27,26 +27,14 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.Stable import androidx.core.content.getSystemService import androidx.core.os.LocaleListCompat -import androidx.datastore.core.DataMigration import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.commons.model.preferences.CopyOnceMigration -import com.vitorpamplona.amethyst.model.AccentColorType -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.FontFamilyType -import com.vitorpamplona.amethyst.model.FontSizeType -import com.vitorpamplona.amethyst.model.ProfileGalleryType -import com.vitorpamplona.amethyst.model.ThemeType -import com.vitorpamplona.amethyst.model.UiSettings -import com.vitorpamplona.amethyst.model.UiSettingsFlow +import com.vitorpamplona.amethyst.commons.model.ThemeType +import com.vitorpamplona.amethyst.commons.model.UiSettings +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow +import com.vitorpamplona.amethyst.commons.model.preferences.UiSettingsStore import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -54,25 +42,43 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import kotlin.coroutines.cancellation.CancellationException -/** The UI settings store. See [UiSharedPreferences.migrations] for the copy it carries. */ +/** + * The file UI, Tor, Namecoin, OTS and the Buzz stores all share, each under its + * own key prefix. + * + * The migration is attached here, at the file, rather than inside + * [UiSettingsStore]: whichever of those stores is constructed first is the one + * that opens the file, and DataStore runs a file's migrations once, on that + * first open. Hanging it off the UI store alone would make the copy depend on + * load order. + */ val Context.sharedPreferencesDataStore: DataStore by preferencesDataStore( name = "shared_settings", - produceMigrations = { UiSharedPreferences.migrations() }, + produceMigrations = { UiSettingsStore.migrations { LocalPreferences.loadSharedSettings() } }, ) +/** + * The Android half of the UI settings: the flows the app observes, and the two + * platform side effects that a theme or language change has to perform. + * + * Persistence is [UiSettingsStore] in `commons`, which every front end shares. + * What stays here is the part that has no desktop equivalent — the per-app night + * mode override that the launch splash reads, and AppCompat's locale list. + */ @Stable class UiSharedPreferences( prefs: UiSettings, val context: Context, val scope: CoroutineScope, ) { + private val store = UiSettingsStore(context.sharedPreferencesDataStore) { LocalPreferences.loadSharedSettings() } + // UI Preferences. Makes sure to wait for it to avoid blinking themes and language preferences val value = UiSettingsFlow.build(prefs) @@ -182,7 +188,7 @@ class UiSharedPreferences( .debounce(1000) .distinctUntilChanged() .onEach { - save(it, context) + store.save(it) }.flowOn(Dispatchers.IO) .stateIn( scope, @@ -191,173 +197,6 @@ class UiSharedPreferences( ) companion object { - // loads faster when individualized - val UI_THEME = stringPreferencesKey("ui.theme") - val UI_LANGUAGE = stringPreferencesKey("ui.language") - val UI_SHOW_IMAGES = stringPreferencesKey("ui.show_images") - val UI_START_PLAYBACK = stringPreferencesKey("ui.start_playback") - val UI_PLAY_VIDEOS = stringPreferencesKey("ui.play_videos") - val UI_SHOW_URL_PREVIEW = stringPreferencesKey("ui.show_url_preview") - val UI_HIDE_NAVIGATION_BARS = stringPreferencesKey("ui.hide_navigation_bars") - val UI_SHOW_PROFILE_PICTURES = stringPreferencesKey("ui.show_profile_pictures") - val UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR = booleanPreferencesKey("ui.dont_show_push_notification_selector") - val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions") - val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set") - val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set") - val UI_PROPOSE_AI_IMPROVEMENTS = stringPreferencesKey("ui.propose_ai_improvements") - val UI_USE_TRACKED_BROADCASTS = stringPreferencesKey("ui.use_tracked_broadcasts") - val UI_AUTOMATICALLY_CREATE_DRAFTS = stringPreferencesKey("ui.automatically_create_drafts") - val UI_SHOW_HOME_NEW_THREADS_TAB = booleanPreferencesKey("ui.show_home_new_threads_tab") - val UI_SHOW_HOME_CONVERSATIONS_TAB = booleanPreferencesKey("ui.show_home_conversations_tab") - val UI_SHOW_HOME_EVERYTHING_TAB = booleanPreferencesKey("ui.show_home_everything_tab") - val UI_SHOW_PROFILE_BADGES = booleanPreferencesKey("ui.show_profile_badges") - val UI_SHOW_PROFILE_APP_RECOMMENDATIONS = booleanPreferencesKey("ui.show_profile_app_recommendations") - val UI_SHOW_PROFILE_ZAP_RECEIVED_FEED = booleanPreferencesKey("ui.show_profile_zap_received_feed") - val UI_SHOW_PROFILE_FOLLOWERS_FEED = booleanPreferencesKey("ui.show_profile_followers_feed") - val UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING = booleanPreferencesKey("ui.dont_show_onchain_public_warning") - val UI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT = stringPreferencesKey("ui.suggest_workouts_from_health_connect") - val UI_ACCENT_COLOR = stringPreferencesKey("ui.accent_color") - val UI_FONT_FAMILY = stringPreferencesKey("ui.font_family") - val UI_FONT_SIZE = stringPreferencesKey("ui.font_size") - val UI_COMPOSE_SIGNATURE = stringPreferencesKey("ui.compose_signature") - val UI_SHOW_ONCHAIN_WALLET = booleanPreferencesKey("ui.show_onchain_wallet") - val UI_SHOW_PAYTO_ZAP_CHIP = booleanPreferencesKey("ui.show_payto_zap_chip") - - suspend fun uiPreferences(context: Context): UiSettings? = - try { - // Get the preference flow and take the first value. - val preferences = context.sharedPreferencesDataStore.data.first() - - val featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED - - UiSettings( - theme = preferences[UI_THEME]?.let { ThemeType.valueOf(it) } ?: ThemeType.SYSTEM, - preferredLanguage = preferences[UI_LANGUAGE]?.ifBlank { null }, - automaticallyShowImages = preferences[UI_SHOW_IMAGES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS, - automaticallyStartPlayback = preferences[UI_START_PLAYBACK]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS, - automaticallyPlayVideos = preferences[UI_PLAY_VIDEOS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, - automaticallyShowUrlPreview = preferences[UI_SHOW_URL_PREVIEW]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS, - automaticallyHideNavigationBars = preferences[UI_HIDE_NAVIGATION_BARS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, - automaticallyShowProfilePictures = preferences[UI_SHOW_PROFILE_PICTURES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS, - dontShowPushNotificationSelector = preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] ?: false, - dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false, - featureSet = featureSet, - gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC, - automaticallyProposeAiImprovements = preferences[UI_PROPOSE_AI_IMPROVEMENTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, - useTrackedBroadcasts = - preferences[UI_USE_TRACKED_BROADCASTS]?.let { BooleanType.valueOf(it) } - ?: if (featureSet == FeatureSetType.COMPLETE) BooleanType.ALWAYS else BooleanType.NEVER, - automaticallyCreateDrafts = preferences[UI_AUTOMATICALLY_CREATE_DRAFTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, - showHomeNewThreadsTab = preferences[UI_SHOW_HOME_NEW_THREADS_TAB] ?: true, - showHomeConversationsTab = preferences[UI_SHOW_HOME_CONVERSATIONS_TAB] ?: true, - showHomeEverythingTab = preferences[UI_SHOW_HOME_EVERYTHING_TAB] ?: false, - showProfileBadges = preferences[UI_SHOW_PROFILE_BADGES] ?: true, - showProfileAppRecommendations = preferences[UI_SHOW_PROFILE_APP_RECOMMENDATIONS] ?: true, - showProfileZapReceivedFeed = preferences[UI_SHOW_PROFILE_ZAP_RECEIVED_FEED] ?: true, - showProfileFollowersFeed = preferences[UI_SHOW_PROFILE_FOLLOWERS_FEED] ?: true, - dontShowOnchainPublicWarning = preferences[UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING] ?: false, - suggestWorkoutsFromHealthConnect = - preferences[UI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, - accentColor = preferences[UI_ACCENT_COLOR]?.let { AccentColorType.valueOf(it) } ?: AccentColorType.PURPLE, - fontFamily = preferences[UI_FONT_FAMILY]?.let { FontFamilyType.valueOf(it) } ?: FontFamilyType.SYSTEM, - fontSize = preferences[UI_FONT_SIZE]?.let { FontSizeType.valueOf(it) } ?: FontSizeType.NORMAL, - composeSignature = preferences[UI_COMPOSE_SIGNATURE] ?: "", - showOnchainWallet = preferences[UI_SHOW_ONCHAIN_WALLET] ?: true, - showPayToZapChip = preferences[UI_SHOW_PAYTO_ZAP_CHIP] ?: true, - ) - } catch (e: Exception) { - if (e is CancellationException) throw e - // Log any errors that occur while reading the DataStore. - Log.e("SharedPreferences") { "Error reading DataStore preferences: ${e.message}" } - - try { - val oldVersion = LocalPreferences.loadSharedSettings() - if (oldVersion != null) { - save(oldVersion, context) - } - oldVersion - } catch (e: Exception) { - if (e is CancellationException) throw e - null - } - } - - /** - * Writes every UI setting into [preferences]. - * - * Shared by [save] and by the one-shot copy out of the old - * `shared_settings` blob, so the two cannot come to disagree about - * which keys a complete set of UI settings has. - */ - internal fun MutablePreferences.write(sharedSettings: UiSettings) { - val preferences = this - preferences[UI_THEME] = sharedSettings.theme.name - preferences[UI_LANGUAGE] = sharedSettings.preferredLanguage ?: "" - preferences[UI_SHOW_IMAGES] = sharedSettings.automaticallyShowImages.name - preferences[UI_START_PLAYBACK] = sharedSettings.automaticallyStartPlayback.name - preferences[UI_PLAY_VIDEOS] = sharedSettings.automaticallyPlayVideos.name - preferences[UI_SHOW_URL_PREVIEW] = sharedSettings.automaticallyShowUrlPreview.name - preferences[UI_HIDE_NAVIGATION_BARS] = sharedSettings.automaticallyHideNavigationBars.name - preferences[UI_SHOW_PROFILE_PICTURES] = sharedSettings.automaticallyShowProfilePictures.name - preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] = sharedSettings.dontShowPushNotificationSelector - preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions - preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name - preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name - preferences[UI_PROPOSE_AI_IMPROVEMENTS] = sharedSettings.automaticallyProposeAiImprovements.name - preferences[UI_USE_TRACKED_BROADCASTS] = sharedSettings.useTrackedBroadcasts.name - preferences[UI_AUTOMATICALLY_CREATE_DRAFTS] = sharedSettings.automaticallyCreateDrafts.name - preferences[UI_SHOW_HOME_NEW_THREADS_TAB] = sharedSettings.showHomeNewThreadsTab - preferences[UI_SHOW_HOME_CONVERSATIONS_TAB] = sharedSettings.showHomeConversationsTab - preferences[UI_SHOW_HOME_EVERYTHING_TAB] = sharedSettings.showHomeEverythingTab - preferences[UI_SHOW_PROFILE_BADGES] = sharedSettings.showProfileBadges - preferences[UI_SHOW_PROFILE_APP_RECOMMENDATIONS] = sharedSettings.showProfileAppRecommendations - preferences[UI_SHOW_PROFILE_ZAP_RECEIVED_FEED] = sharedSettings.showProfileZapReceivedFeed - preferences[UI_SHOW_PROFILE_FOLLOWERS_FEED] = sharedSettings.showProfileFollowersFeed - preferences[UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING] = sharedSettings.dontShowOnchainPublicWarning - preferences[UI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT] = sharedSettings.suggestWorkoutsFromHealthConnect.name - preferences[UI_ACCENT_COLOR] = sharedSettings.accentColor.name - preferences[UI_FONT_FAMILY] = sharedSettings.fontFamily.name - preferences[UI_FONT_SIZE] = sharedSettings.fontSize.name - preferences[UI_COMPOSE_SIGNATURE] = sharedSettings.composeSignature - preferences[UI_SHOW_ONCHAIN_WALLET] = sharedSettings.showOnchainWallet - preferences[UI_SHOW_PAYTO_ZAP_CHIP] = sharedSettings.showPayToZapChip - } - - /** - * The one-shot copy out of the single `shared_settings` JSON blob these - * settings used to be kept as, in the global encrypted file. - * - * Guarded, and it has to be. Unlike the per-account migrations, this - * store has been the real home of these settings for a while, so most - * installs already have a populated one — and copying an old blob over - * it would undo every UI change the user has made since. [UI_THEME] is - * the test: [save] writes every key unconditionally and is the only - * writer, so its absence means this store has never been saved, which - * is exactly the install whose settings are still only in the legacy - * file. - */ - internal fun migrations(): List> = - listOf( - CopyOnceMigration("migrated.sharedSettings") { out -> - if (out[UI_THEME] == null) { - withContext(Dispatchers.IO) { - LocalPreferences.loadSharedSettings()?.let { out.write(it) } - } - } - }, - ) - - suspend fun save( - sharedSettings: UiSettings, - context: Context, - ) { - try { - context.sharedPreferencesDataStore.edit { preferences -> preferences.write(sharedSettings) } - } catch (e: Exception) { - if (e is CancellationException) throw e - // Log any errors that occur while reading the DataStore. - Log.e("SharedPreferences") { "Error saving DataStore preferences: ${e.message}" } - } - } + suspend fun uiPreferences(context: Context): UiSettings? = UiSettingsStore(context.sharedPreferencesDataStore) { LocalPreferences.loadSharedSettings() }.load() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt index 382242d133..e4619c6624 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt @@ -25,10 +25,10 @@ import android.content.Intent import android.content.res.Configuration import android.os.Bundle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.napplet.NappletArtifactPolicy import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity -import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletHostActivity import com.vitorpamplona.amethyst.napplethost.NappletHostContract diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt index 44773b05a0..1c9c1f1772 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt @@ -38,9 +38,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.BooleanType import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastEvent import com.vitorpamplona.amethyst.commons.service.pow.PoWJobState -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt index fb12ae4b2d..00689c692d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -29,12 +29,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.FeatureSetType import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations import com.vitorpamplona.amethyst.ui.note.FirstUserInfoRow import com.vitorpamplona.amethyst.ui.note.types.EditState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt index ea6ed3fce7..a68e877ea5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.ProfileGalleryType -import com.vitorpamplona.amethyst.model.UiSettingsFlow +import com.vitorpamplona.amethyst.commons.model.BooleanType +import com.vitorpamplona.amethyst.commons.model.ConnectivityType +import com.vitorpamplona.amethyst.commons.model.FeatureSetType +import com.vitorpamplona.amethyst.commons.model.ProfileGalleryType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 57381f76b9..23ac016ee0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Dao import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflict import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -111,7 +112,6 @@ import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LatestKeyPackageOwner -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder import com.vitorpamplona.amethyst.service.ClinkDebitPayer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt index fab2842f38..8422c85477 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt @@ -27,9 +27,9 @@ import android.os.Bundle import androidx.annotation.RequiresApi import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.tor.TorType import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher -import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry import com.vitorpamplona.amethyst.napplethost.NappletHostContract import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedWebAppController diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabThemeWatcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabThemeWatcher.kt index 9c0ddcf11a..d3e8c770f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabThemeWatcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabThemeWatcher.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.model.ThemeType +import com.vitorpamplona.amethyst.commons.model.ThemeType /** * Keeps the warm embedded tabs in sync with the app's DARK/LIGHT theme. An embed WebView resolves its diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 65832e94fb..8c5bd12fec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -81,6 +81,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.BooleanType import com.vitorpamplona.amethyst.commons.model.composer.AudienceSelection import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList @@ -134,7 +135,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 72bc69b916..08df10a49e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -35,6 +35,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.BooleanType import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -69,7 +70,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory import com.vitorpamplona.amethyst.service.location.LocationState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index f7227a9362..cd772a90de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.notification_tab_everyone @@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 4f580033c9..e3010ffd5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -73,6 +73,14 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.AccentColorType +import com.vitorpamplona.amethyst.commons.model.BooleanType +import com.vitorpamplona.amethyst.commons.model.ConnectivityType +import com.vitorpamplona.amethyst.commons.model.FeatureSetType +import com.vitorpamplona.amethyst.commons.model.FontFamilyType +import com.vitorpamplona.amethyst.commons.model.FontSizeType +import com.vitorpamplona.amethyst.commons.model.ThemeType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.accent_color import com.vitorpamplona.amethyst.commons.resources.accent_color_description @@ -118,18 +126,11 @@ import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.settings.resourceId import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.contentColorOnAccent import com.vitorpamplona.amethyst.commons.ui.theme.isLight -import com.vitorpamplona.amethyst.model.AccentColorType -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.FontFamilyType -import com.vitorpamplona.amethyst.model.FontSizeType -import com.vitorpamplona.amethyst.model.ThemeType -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.previewColor import com.vitorpamplona.amethyst.ui.theme.toFontFamily diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index 933b81c1ee..3f0691f2b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt @@ -45,6 +45,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.BooleanType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.add_client_tag_explainer import com.vitorpamplona.amethyst.commons.resources.add_client_tag_title @@ -96,8 +98,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.model.AccountPoWPreferences -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory import com.vitorpamplona.amethyst.service.pow.deviceHashesPerSecond import com.vitorpamplona.amethyst.service.pow.loadApproxDuration diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt index 838ca8b799..ba83376d7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt @@ -36,6 +36,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.HomeFeedType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.conversations import com.vitorpamplona.amethyst.commons.resources.home_content_type_articles @@ -72,7 +73,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt index e533d60817..3fa6edba83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt @@ -34,6 +34,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.ProfileGalleryType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.gallery_style import com.vitorpamplona.amethyst.commons.resources.gallery_style_description @@ -48,10 +50,9 @@ import com.vitorpamplona.amethyst.commons.resources.settings_section_profile_sec import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.settings.resourceId import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow -import com.vitorpamplona.amethyst.model.ProfileGalleryType -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 0e38c36ff5..7a5d836238 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -38,7 +38,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.WarningType import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.model.parseWarningType import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.blocked_users import com.vitorpamplona.amethyst.commons.resources.filter_spam_from_strangers_explainer @@ -63,10 +65,9 @@ import com.vitorpamplona.amethyst.commons.resources.warn_when_posts_have_reports import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.settings.resourceId import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.model.WarningType -import com.vitorpamplona.amethyst.model.parseWarningType import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/DetectedWorkoutCarousel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/DetectedWorkoutCarousel.kt index fb28dfd9fd..ffc23b3d25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/DetectedWorkoutCarousel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/DetectedWorkoutCarousel.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.fitness.TrainingLog import com.vitorpamplona.amethyst.commons.fitness.WorkoutOrigin import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.BooleanType import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.workout_from_health_connect @@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.commons.resources.workout_suggestion_connect_t import com.vitorpamplona.amethyst.commons.resources.workout_suggestion_distance_km import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.service.workouts.health.HealthConnectManager import com.vitorpamplona.amethyst.service.workouts.health.publishedWorkoutsOf import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 13a092c162..7db63b6a55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -44,6 +44,10 @@ import com.patrykandpatrick.vico.compose.common.VicoTheme import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.ProvideAppIcons +import com.vitorpamplona.amethyst.commons.model.AccentColorType +import com.vitorpamplona.amethyst.commons.model.FontFamilyType +import com.vitorpamplona.amethyst.commons.model.FontSizeType +import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueDark import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueLight import com.vitorpamplona.amethyst.commons.ui.theme.AccentGreenDark @@ -64,10 +68,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.amethystLightColorScheme import com.vitorpamplona.amethyst.commons.ui.theme.isLight import com.vitorpamplona.amethyst.commons.ui.theme.transparentBackground import com.vitorpamplona.amethyst.commons.ui.theme.withFontFamily -import com.vitorpamplona.amethyst.model.AccentColorType -import com.vitorpamplona.amethyst.model.FontFamilyType -import com.vitorpamplona.amethyst.model.FontSizeType -import com.vitorpamplona.amethyst.model.ThemeType // The accent color (primary/secondary/tertiary) is user-selectable in Settings -> Accent Color. // Purple keeps the original Amethyst look (purple primary + teal secondary). Every other accent diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index dc32830cc4..0915fcb2d2 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -29,7 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.vitorpamplona.amethyst.model.UiSettingsFlow +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow @OptIn(ExperimentalPermissionsApi::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettings.kt similarity index 66% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettings.kt index 159afa0514..eb8ae128e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettings.kt @@ -18,40 +18,10 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model +package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.accent_color_blue -import com.vitorpamplona.amethyst.commons.resources.accent_color_green -import com.vitorpamplona.amethyst.commons.resources.accent_color_orange -import com.vitorpamplona.amethyst.commons.resources.accent_color_pink -import com.vitorpamplona.amethyst.commons.resources.accent_color_purple -import com.vitorpamplona.amethyst.commons.resources.accent_color_red -import com.vitorpamplona.amethyst.commons.resources.connectivity_type_always -import com.vitorpamplona.amethyst.commons.resources.connectivity_type_never -import com.vitorpamplona.amethyst.commons.resources.connectivity_type_unmetered_wifi_only -import com.vitorpamplona.amethyst.commons.resources.content_warning_hide_all_sensitive_content_option -import com.vitorpamplona.amethyst.commons.resources.content_warning_see_warnings_option -import com.vitorpamplona.amethyst.commons.resources.content_warning_show_all_sensitive_content_option -import com.vitorpamplona.amethyst.commons.resources.dark -import com.vitorpamplona.amethyst.commons.resources.font_family_monospace -import com.vitorpamplona.amethyst.commons.resources.font_family_sans_serif -import com.vitorpamplona.amethyst.commons.resources.font_family_serif -import com.vitorpamplona.amethyst.commons.resources.font_family_system -import com.vitorpamplona.amethyst.commons.resources.font_size_huge -import com.vitorpamplona.amethyst.commons.resources.font_size_large -import com.vitorpamplona.amethyst.commons.resources.font_size_normal -import com.vitorpamplona.amethyst.commons.resources.font_size_small -import com.vitorpamplona.amethyst.commons.resources.gallery_type_classic -import com.vitorpamplona.amethyst.commons.resources.gallery_type_modern -import com.vitorpamplona.amethyst.commons.resources.light -import com.vitorpamplona.amethyst.commons.resources.system -import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_complete -import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_performance -import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_simplified import kotlinx.serialization.Serializable -import org.jetbrains.compose.resources.StringResource @Stable @Serializable @@ -98,11 +68,10 @@ data class UiSettings( enum class ThemeType( val screenCode: Int, - val resourceId: StringResource, ) { - SYSTEM(0, Res.string.system), - LIGHT(1, Res.string.light), - DARK(2, Res.string.dark), + SYSTEM(0), + LIGHT(1), + DARK(2), } fun parseThemeType(code: Int?): ThemeType = @@ -115,14 +84,13 @@ fun parseThemeType(code: Int?): ThemeType = enum class AccentColorType( val screenCode: Int, - val resourceId: StringResource, ) { - PURPLE(0, Res.string.accent_color_purple), - BLUE(1, Res.string.accent_color_blue), - GREEN(2, Res.string.accent_color_green), - ORANGE(3, Res.string.accent_color_orange), - RED(4, Res.string.accent_color_red), - PINK(5, Res.string.accent_color_pink), + PURPLE(0), + BLUE(1), + GREEN(2), + ORANGE(3), + RED(4), + PINK(5), } fun parseAccentColorType(screenCode: Int): AccentColorType = @@ -138,12 +106,11 @@ fun parseAccentColorType(screenCode: Int): AccentColorType = enum class FontFamilyType( val screenCode: Int, - val resourceId: StringResource, ) { - SYSTEM(0, Res.string.font_family_system), - SANS_SERIF(1, Res.string.font_family_sans_serif), - SERIF(2, Res.string.font_family_serif), - MONOSPACE(3, Res.string.font_family_monospace), + SYSTEM(0), + SANS_SERIF(1), + SERIF(2), + MONOSPACE(3), } fun parseFontFamilyType(screenCode: Int): FontFamilyType = @@ -158,12 +125,11 @@ fun parseFontFamilyType(screenCode: Int): FontFamilyType = enum class FontSizeType( val scale: Float, val screenCode: Int, - val resourceId: StringResource, ) { - SMALL(0.85f, 0, Res.string.font_size_small), - NORMAL(1.0f, 1, Res.string.font_size_normal), - LARGE(1.15f, 2, Res.string.font_size_large), - HUGE(1.3f, 3, Res.string.font_size_huge), + SMALL(0.85f, 0), + NORMAL(1.0f, 1), + LARGE(1.15f, 2), + HUGE(1.3f, 3), } fun parseFontSizeType(screenCode: Int): FontSizeType = @@ -178,28 +144,25 @@ fun parseFontSizeType(screenCode: Int): FontSizeType = enum class ConnectivityType( val prefCode: Boolean?, val screenCode: Int, - val resourceId: StringResource, ) { - ALWAYS(null, 0, Res.string.connectivity_type_always), - WIFI_ONLY(true, 1, Res.string.connectivity_type_unmetered_wifi_only), - NEVER(false, 2, Res.string.connectivity_type_never), + ALWAYS(null, 0), + WIFI_ONLY(true, 1), + NEVER(false, 2), } enum class FeatureSetType( val screenCode: Int, - val resourceId: StringResource, ) { - COMPLETE(0, Res.string.ui_feature_set_type_complete), - SIMPLIFIED(1, Res.string.ui_feature_set_type_simplified), - PERFORMANCE(2, Res.string.ui_feature_set_type_performance), + COMPLETE(0), + SIMPLIFIED(1), + PERFORMANCE(2), } enum class ProfileGalleryType( val screenCode: Int, - val resourceId: StringResource, ) { - CLASSIC(0, Res.string.gallery_type_classic), - MODERN(1, Res.string.gallery_type_modern), + CLASSIC(0), + MODERN(1), } fun parseConnectivityType(code: Boolean?): ConnectivityType = @@ -236,10 +199,9 @@ fun parseGalleryType(screenCode: Int): ProfileGalleryType = enum class BooleanType( val prefCode: Boolean?, val screenCode: Int, - val reourceId: StringResource, ) { - ALWAYS(null, 0, Res.string.connectivity_type_always), - NEVER(false, 1, Res.string.connectivity_type_never), + ALWAYS(null, 0), + NEVER(false, 1), } fun parseBooleanType(code: Boolean?): BooleanType = @@ -259,11 +221,10 @@ fun parseBooleanType(screenCode: Int): BooleanType = enum class WarningType( val prefCode: Boolean?, val screenCode: Int, - val resourceId: StringResource, ) { - WARN(null, 0, Res.string.content_warning_see_warnings_option), - SHOW(true, 1, Res.string.content_warning_show_all_sensitive_content_option), - HIDE(false, 2, Res.string.content_warning_hide_all_sensitive_content_option), + WARN(null, 0), + SHOW(true, 1), + HIDE(false, 2), } fun parseWarningType(screenCode: Int): WarningType = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsFlow.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsFlow.kt index d2528689c2..6a0870cfa8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsFlow.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model +package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Stable import kotlinx.coroutines.flow.Flow diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/UiSettingsStore.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/UiSettingsStore.kt new file mode 100644 index 0000000000..c1606b0440 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/UiSettingsStore.kt @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model.preferences + +import androidx.datastore.core.DataMigration +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.vitorpamplona.amethyst.commons.model.AccentColorType +import com.vitorpamplona.amethyst.commons.model.BooleanType +import com.vitorpamplona.amethyst.commons.model.ConnectivityType +import com.vitorpamplona.amethyst.commons.model.FeatureSetType +import com.vitorpamplona.amethyst.commons.model.FontFamilyType +import com.vitorpamplona.amethyst.commons.model.FontSizeType +import com.vitorpamplona.amethyst.commons.model.ProfileGalleryType +import com.vitorpamplona.amethyst.commons.model.ThemeType +import com.vitorpamplona.amethyst.commons.model.UiSettings +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import kotlin.coroutines.cancellation.CancellationException + +/** + * The app-wide UI settings — theme, language, what loads on cellular, which + * profile and home tabs are shown. + * + * Lives on the [AppPreferenceStores.SHARED_SETTINGS] file under the `ui.` key + * prefix, one key per setting rather than a single blob: a DataStore read is + * whole-file anyway, but individual keys mean a setting added later does not + * invalidate the ones already stored. + * + * Headless on purpose. Applying a theme or a locale is platform work — on + * Android it is `UiModeManager` and `AppCompatDelegate`, which have no desktop + * equivalent — so that half stays in each front end and only the persistence + * is shared. + * + * @param loadLegacy reads the single `shared_settings` JSON blob these settings + * used to live in. Injected because the legacy file is Android's, and this + * store is not. Front ends without one pass nothing. + */ +class UiSettingsStore( + private val store: DataStore, + private val loadLegacy: suspend () -> UiSettings? = { null }, +) { + /** + * Reads every setting, falling back to the legacy blob if the store itself + * cannot be read. + * + * Returns null when neither can be read, which the caller shows as "still + * loading" rather than as defaults — writing defaults over an unreadable + * store would make a transient failure permanent on the next save. + */ + suspend fun load(): UiSettings? = + try { + read(store.data.first()) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("UiSettingsStore") { "Error reading DataStore preferences: ${e.message}" } + + try { + loadLegacy()?.also { save(it) } + } catch (e: Exception) { + if (e is CancellationException) throw e + null + } + } + + suspend fun save(settings: UiSettings) { + try { + store.edit { it.write(settings) } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("UiSettingsStore") { "Error saving DataStore preferences: ${e.message}" } + } + } + + companion object { + // loads faster when individualized + val UI_THEME = stringPreferencesKey("ui.theme") + val UI_LANGUAGE = stringPreferencesKey("ui.language") + val UI_SHOW_IMAGES = stringPreferencesKey("ui.show_images") + val UI_START_PLAYBACK = stringPreferencesKey("ui.start_playback") + val UI_PLAY_VIDEOS = stringPreferencesKey("ui.play_videos") + val UI_SHOW_URL_PREVIEW = stringPreferencesKey("ui.show_url_preview") + val UI_HIDE_NAVIGATION_BARS = stringPreferencesKey("ui.hide_navigation_bars") + val UI_SHOW_PROFILE_PICTURES = stringPreferencesKey("ui.show_profile_pictures") + val UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR = booleanPreferencesKey("ui.dont_show_push_notification_selector") + val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions") + val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set") + val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set") + val UI_PROPOSE_AI_IMPROVEMENTS = stringPreferencesKey("ui.propose_ai_improvements") + val UI_USE_TRACKED_BROADCASTS = stringPreferencesKey("ui.use_tracked_broadcasts") + val UI_AUTOMATICALLY_CREATE_DRAFTS = stringPreferencesKey("ui.automatically_create_drafts") + val UI_SHOW_HOME_NEW_THREADS_TAB = booleanPreferencesKey("ui.show_home_new_threads_tab") + val UI_SHOW_HOME_CONVERSATIONS_TAB = booleanPreferencesKey("ui.show_home_conversations_tab") + val UI_SHOW_HOME_EVERYTHING_TAB = booleanPreferencesKey("ui.show_home_everything_tab") + val UI_SHOW_PROFILE_BADGES = booleanPreferencesKey("ui.show_profile_badges") + val UI_SHOW_PROFILE_APP_RECOMMENDATIONS = booleanPreferencesKey("ui.show_profile_app_recommendations") + val UI_SHOW_PROFILE_ZAP_RECEIVED_FEED = booleanPreferencesKey("ui.show_profile_zap_received_feed") + val UI_SHOW_PROFILE_FOLLOWERS_FEED = booleanPreferencesKey("ui.show_profile_followers_feed") + val UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING = booleanPreferencesKey("ui.dont_show_onchain_public_warning") + val UI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT = stringPreferencesKey("ui.suggest_workouts_from_health_connect") + val UI_ACCENT_COLOR = stringPreferencesKey("ui.accent_color") + val UI_FONT_FAMILY = stringPreferencesKey("ui.font_family") + val UI_FONT_SIZE = stringPreferencesKey("ui.font_size") + val UI_COMPOSE_SIGNATURE = stringPreferencesKey("ui.compose_signature") + val UI_SHOW_ONCHAIN_WALLET = booleanPreferencesKey("ui.show_onchain_wallet") + val UI_SHOW_PAYTO_ZAP_CHIP = booleanPreferencesKey("ui.show_payto_zap_chip") + + /** + * Every setting's default matches what the old `getBoolean(key, default)` + * call returned for a missing key. Several of them are `true`, so reading + * a default of `false` here would silently turn features off for every + * install that never touched them. + */ + fun read(preferences: Preferences): UiSettings { + val featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED + + return UiSettings( + theme = preferences[UI_THEME]?.let { ThemeType.valueOf(it) } ?: ThemeType.SYSTEM, + preferredLanguage = preferences[UI_LANGUAGE]?.ifBlank { null }, + automaticallyShowImages = preferences[UI_SHOW_IMAGES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS, + automaticallyStartPlayback = preferences[UI_START_PLAYBACK]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS, + automaticallyPlayVideos = preferences[UI_PLAY_VIDEOS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, + automaticallyShowUrlPreview = preferences[UI_SHOW_URL_PREVIEW]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS, + automaticallyHideNavigationBars = preferences[UI_HIDE_NAVIGATION_BARS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, + automaticallyShowProfilePictures = preferences[UI_SHOW_PROFILE_PICTURES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS, + dontShowPushNotificationSelector = preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] ?: false, + dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false, + featureSet = featureSet, + gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC, + automaticallyProposeAiImprovements = preferences[UI_PROPOSE_AI_IMPROVEMENTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, + useTrackedBroadcasts = + preferences[UI_USE_TRACKED_BROADCASTS]?.let { BooleanType.valueOf(it) } + ?: if (featureSet == FeatureSetType.COMPLETE) BooleanType.ALWAYS else BooleanType.NEVER, + automaticallyCreateDrafts = preferences[UI_AUTOMATICALLY_CREATE_DRAFTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, + showHomeNewThreadsTab = preferences[UI_SHOW_HOME_NEW_THREADS_TAB] ?: true, + showHomeConversationsTab = preferences[UI_SHOW_HOME_CONVERSATIONS_TAB] ?: true, + showHomeEverythingTab = preferences[UI_SHOW_HOME_EVERYTHING_TAB] ?: false, + showProfileBadges = preferences[UI_SHOW_PROFILE_BADGES] ?: true, + showProfileAppRecommendations = preferences[UI_SHOW_PROFILE_APP_RECOMMENDATIONS] ?: true, + showProfileZapReceivedFeed = preferences[UI_SHOW_PROFILE_ZAP_RECEIVED_FEED] ?: true, + showProfileFollowersFeed = preferences[UI_SHOW_PROFILE_FOLLOWERS_FEED] ?: true, + dontShowOnchainPublicWarning = preferences[UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING] ?: false, + suggestWorkoutsFromHealthConnect = + preferences[UI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, + accentColor = preferences[UI_ACCENT_COLOR]?.let { AccentColorType.valueOf(it) } ?: AccentColorType.PURPLE, + fontFamily = preferences[UI_FONT_FAMILY]?.let { FontFamilyType.valueOf(it) } ?: FontFamilyType.SYSTEM, + fontSize = preferences[UI_FONT_SIZE]?.let { FontSizeType.valueOf(it) } ?: FontSizeType.NORMAL, + composeSignature = preferences[UI_COMPOSE_SIGNATURE] ?: "", + showOnchainWallet = preferences[UI_SHOW_ONCHAIN_WALLET] ?: true, + showPayToZapChip = preferences[UI_SHOW_PAYTO_ZAP_CHIP] ?: true, + ) + } + + /** + * Writes every UI setting into [this]. + * + * Shared by [save] and by the one-shot copy out of the old + * `shared_settings` blob, so the two cannot come to disagree about + * which keys a complete set of UI settings has. + */ + fun MutablePreferences.write(sharedSettings: UiSettings) { + val preferences = this + preferences[UI_THEME] = sharedSettings.theme.name + preferences[UI_LANGUAGE] = sharedSettings.preferredLanguage ?: "" + preferences[UI_SHOW_IMAGES] = sharedSettings.automaticallyShowImages.name + preferences[UI_START_PLAYBACK] = sharedSettings.automaticallyStartPlayback.name + preferences[UI_PLAY_VIDEOS] = sharedSettings.automaticallyPlayVideos.name + preferences[UI_SHOW_URL_PREVIEW] = sharedSettings.automaticallyShowUrlPreview.name + preferences[UI_HIDE_NAVIGATION_BARS] = sharedSettings.automaticallyHideNavigationBars.name + preferences[UI_SHOW_PROFILE_PICTURES] = sharedSettings.automaticallyShowProfilePictures.name + preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] = sharedSettings.dontShowPushNotificationSelector + preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions + preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name + preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name + preferences[UI_PROPOSE_AI_IMPROVEMENTS] = sharedSettings.automaticallyProposeAiImprovements.name + preferences[UI_USE_TRACKED_BROADCASTS] = sharedSettings.useTrackedBroadcasts.name + preferences[UI_AUTOMATICALLY_CREATE_DRAFTS] = sharedSettings.automaticallyCreateDrafts.name + preferences[UI_SHOW_HOME_NEW_THREADS_TAB] = sharedSettings.showHomeNewThreadsTab + preferences[UI_SHOW_HOME_CONVERSATIONS_TAB] = sharedSettings.showHomeConversationsTab + preferences[UI_SHOW_HOME_EVERYTHING_TAB] = sharedSettings.showHomeEverythingTab + preferences[UI_SHOW_PROFILE_BADGES] = sharedSettings.showProfileBadges + preferences[UI_SHOW_PROFILE_APP_RECOMMENDATIONS] = sharedSettings.showProfileAppRecommendations + preferences[UI_SHOW_PROFILE_ZAP_RECEIVED_FEED] = sharedSettings.showProfileZapReceivedFeed + preferences[UI_SHOW_PROFILE_FOLLOWERS_FEED] = sharedSettings.showProfileFollowersFeed + preferences[UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING] = sharedSettings.dontShowOnchainPublicWarning + preferences[UI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT] = sharedSettings.suggestWorkoutsFromHealthConnect.name + preferences[UI_ACCENT_COLOR] = sharedSettings.accentColor.name + preferences[UI_FONT_FAMILY] = sharedSettings.fontFamily.name + preferences[UI_FONT_SIZE] = sharedSettings.fontSize.name + preferences[UI_COMPOSE_SIGNATURE] = sharedSettings.composeSignature + preferences[UI_SHOW_ONCHAIN_WALLET] = sharedSettings.showOnchainWallet + preferences[UI_SHOW_PAYTO_ZAP_CHIP] = sharedSettings.showPayToZapChip + } + + /** + * The one-shot copy out of the single `shared_settings` JSON blob these + * settings used to be kept as, in the global encrypted file. + * + * Guarded, and it has to be. Unlike the per-account migrations, this + * store has been the real home of these settings for a while, so most + * installs already have a populated one — and copying an old blob over + * it would undo every UI change the user has made since. [UI_THEME] is + * the test: [write] sets every key unconditionally and is the only + * writer, so its absence means this store has never been saved, which + * is exactly the install whose settings are still only in the legacy + * file. + * + * Must be handed to the store at construction, so every consumer of the + * shared file gets it no matter which one opens the file first. + */ + fun migrations(loadLegacy: suspend () -> UiSettings?): List> = + listOf( + CopyOnceMigration("migrated.sharedSettings") { out -> + if (out[UI_THEME] == null) { + withContext(Dispatchers.IO) { + loadLegacy()?.let { out.write(it) } + } + } + }, + ) + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/UiSettingsLabels.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/UiSettingsLabels.kt new file mode 100644 index 0000000000..247c0c8ebc --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/UiSettingsLabels.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.settings + +import com.vitorpamplona.amethyst.commons.model.AccentColorType +import com.vitorpamplona.amethyst.commons.model.FontSizeType +import com.vitorpamplona.amethyst.commons.model.ProfileGalleryType +import com.vitorpamplona.amethyst.commons.model.ThemeType +import com.vitorpamplona.amethyst.commons.model.WarningType +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.accent_color_blue +import com.vitorpamplona.amethyst.commons.resources.accent_color_green +import com.vitorpamplona.amethyst.commons.resources.accent_color_orange +import com.vitorpamplona.amethyst.commons.resources.accent_color_pink +import com.vitorpamplona.amethyst.commons.resources.accent_color_purple +import com.vitorpamplona.amethyst.commons.resources.accent_color_red +import com.vitorpamplona.amethyst.commons.resources.content_warning_hide_all_sensitive_content_option +import com.vitorpamplona.amethyst.commons.resources.content_warning_see_warnings_option +import com.vitorpamplona.amethyst.commons.resources.content_warning_show_all_sensitive_content_option +import com.vitorpamplona.amethyst.commons.resources.dark +import com.vitorpamplona.amethyst.commons.resources.font_size_huge +import com.vitorpamplona.amethyst.commons.resources.font_size_large +import com.vitorpamplona.amethyst.commons.resources.font_size_normal +import com.vitorpamplona.amethyst.commons.resources.font_size_small +import com.vitorpamplona.amethyst.commons.resources.gallery_type_classic +import com.vitorpamplona.amethyst.commons.resources.gallery_type_modern +import com.vitorpamplona.amethyst.commons.resources.light +import com.vitorpamplona.amethyst.commons.resources.system +import org.jetbrains.compose.resources.StringResource + +/** + * The display label for each UI-settings enum. + * + * These used to be a constructor argument on the enums themselves, which pinned + * `UiSettings` to `commonsUI` — a [StringResource] comes from the generated `Res` + * class, and `commons` cannot see it. The settings are plain data that the CLI and + * any headless front end may read, so the data moved to `commons/model` and the + * labels stayed here, following the same extension-property shape the Tor settings + * already use (`ui.tor.resourceId`). + * + * Each `when` is exhaustive over its enum, so a new constant is a compile error + * here rather than a missing label at runtime — the same guarantee the constructor + * argument gave. + * + * Only the enums something actually labels are here. `ConnectivityType`, + * `FeatureSetType` and `FontFamilyType` carried a `resourceId` that nothing read — + * their pickers are segmented rows and use the narrower `shortLabelRes` helpers in + * `AppSettingsScreen` instead — and `BooleanType`'s was misspelled `reourceId`, + * which is how it went unnoticed. They are dropped rather than carried over; the + * strings they pointed at are still used by those short labels. + */ +val ThemeType.resourceId: StringResource + get() = + when (this) { + ThemeType.SYSTEM -> Res.string.system + ThemeType.LIGHT -> Res.string.light + ThemeType.DARK -> Res.string.dark + } + +val AccentColorType.resourceId: StringResource + get() = + when (this) { + AccentColorType.PURPLE -> Res.string.accent_color_purple + AccentColorType.BLUE -> Res.string.accent_color_blue + AccentColorType.GREEN -> Res.string.accent_color_green + AccentColorType.ORANGE -> Res.string.accent_color_orange + AccentColorType.RED -> Res.string.accent_color_red + AccentColorType.PINK -> Res.string.accent_color_pink + } + +val FontSizeType.resourceId: StringResource + get() = + when (this) { + FontSizeType.SMALL -> Res.string.font_size_small + FontSizeType.NORMAL -> Res.string.font_size_normal + FontSizeType.LARGE -> Res.string.font_size_large + FontSizeType.HUGE -> Res.string.font_size_huge + } + +val ProfileGalleryType.resourceId: StringResource + get() = + when (this) { + ProfileGalleryType.CLASSIC -> Res.string.gallery_type_classic + ProfileGalleryType.MODERN -> Res.string.gallery_type_modern + } + +val WarningType.resourceId: StringResource + get() = + when (this) { + WarningType.WARN -> Res.string.content_warning_see_warnings_option + WarningType.SHOW -> Res.string.content_warning_show_all_sensitive_content_option + WarningType.HIDE -> Res.string.content_warning_hide_all_sensitive_content_option + }