2 Commits

Author SHA1 Message Date
6bb4ef2805 add settings viewmodel 2026-07-20 08:52:38 +07:00
e4e73da229 update settings 2026-07-20 08:32:34 +07:00
8 changed files with 149 additions and 20 deletions

View File

@@ -39,6 +39,7 @@ import androidx.navigation3.ui.NavDisplay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import su.reya.coop.repository.AccountRepository import su.reya.coop.repository.AccountRepository
import su.reya.coop.repository.ChatRepository import su.reya.coop.repository.ChatRepository
import su.reya.coop.repository.SettingsRepository
import su.reya.coop.screens.ContactListScreen import su.reya.coop.screens.ContactListScreen
import su.reya.coop.screens.HomeScreen import su.reya.coop.screens.HomeScreen
import su.reya.coop.screens.ImportScreen import su.reya.coop.screens.ImportScreen
@@ -56,11 +57,16 @@ import su.reya.coop.viewmodel.AccountViewModel
import su.reya.coop.viewmodel.ChatScreenViewModel import su.reya.coop.viewmodel.ChatScreenViewModel
import su.reya.coop.viewmodel.ChatViewModel import su.reya.coop.viewmodel.ChatViewModel
import su.reya.coop.viewmodel.ProfileCache import su.reya.coop.viewmodel.ProfileCache
import su.reya.coop.viewmodel.SettingsViewModel
val LocalProfileCache = staticCompositionLocalOf<ProfileCache> { val LocalProfileCache = staticCompositionLocalOf<ProfileCache> {
error("No ProfileCache provided") error("No ProfileCache provided")
} }
val LocalSettings = staticCompositionLocalOf<Settings> {
error("No Settings provided")
}
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> { val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
error("No SnackbarHostState provided") error("No SnackbarHostState provided")
} }
@@ -79,6 +85,7 @@ fun App(
profileCache: ProfileCache, profileCache: ProfileCache,
accountRepository: AccountRepository, accountRepository: AccountRepository,
chatRepository: ChatRepository, chatRepository: ChatRepository,
settingsRepository: SettingsRepository,
) { ) {
val viewModelFactory = remember { val viewModelFactory = remember {
object : ViewModelProvider.Factory { object : ViewModelProvider.Factory {
@@ -92,6 +99,10 @@ fun App(
accountRepository accountRepository
) )
modelClass.isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(
settingsRepository
)
else -> throw IllegalArgumentException("Unknown ViewModel class") else -> throw IllegalArgumentException("Unknown ViewModel class")
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -102,6 +113,7 @@ fun App(
val accountViewModel: AccountViewModel = viewModel(factory = viewModelFactory) val accountViewModel: AccountViewModel = viewModel(factory = viewModelFactory)
val chatViewModel: ChatViewModel = viewModel(factory = viewModelFactory) val chatViewModel: ChatViewModel = viewModel(factory = viewModelFactory)
val settingsViewModel: SettingsViewModel = viewModel(factory = viewModelFactory)
val context = LocalContext.current val context = LocalContext.current
val activity = context as? ComponentActivity val activity = context as? ComponentActivity
@@ -113,19 +125,24 @@ fun App(
val accountState by accountViewModel.state.collectAsStateWithLifecycle() val accountState by accountViewModel.state.collectAsStateWithLifecycle()
val signerRequired = accountState.signerRequired val signerRequired = accountState.signerRequired
// Get the settings
val settings by settingsViewModel.settings.collectAsStateWithLifecycle()
// Snackbar // Snackbar
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
// Check if dark theme enabled // Check if dark theme enabled
val darkMode = isSystemInDarkTheme() val darkMode = when (settings.theme) {
Theme.Light -> false
Theme.Dark -> true
Theme.System -> isSystemInDarkTheme()
}
// Enabled the dynamic color scheme // Enabled the dynamic color scheme
val colorScheme = when { val colorScheme = when {
// Enable the dynamic color scheme for Android 12+ // Enable the dynamic color scheme for Android 12+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { settings.dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (isSystemInDarkTheme()) dynamicDarkColorScheme(context) else dynamicLightColorScheme( if (darkMode) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
context
)
} }
// When dark mode is enabled, use the dark color scheme // When dark mode is enabled, use the dark color scheme
darkMode -> darkColorScheme() darkMode -> darkColorScheme()
@@ -190,11 +207,11 @@ fun App(
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalProfileCache provides profileCache, LocalProfileCache provides profileCache,
LocalSettings provides settings,
LocalSnackbarHostState provides snackbarHostState, LocalSnackbarHostState provides snackbarHostState,
LocalNavigator provides navigator, LocalNavigator provides navigator,
LocalScanResult provides qrScanResult, LocalScanResult provides qrScanResult,
) { ) {
NavDisplay( NavDisplay(
backStack = backStack, backStack = backStack,
onBack = { onBack = {

View File

@@ -14,6 +14,7 @@ import su.reya.coop.nostr.NostrManager
import su.reya.coop.repository.AccountRepository import su.reya.coop.repository.AccountRepository
import su.reya.coop.repository.ChatRepository import su.reya.coop.repository.ChatRepository
import su.reya.coop.repository.MediaRepository import su.reya.coop.repository.MediaRepository
import su.reya.coop.repository.SettingsRepository
import su.reya.coop.viewmodel.ProfileCache import su.reya.coop.viewmodel.ProfileCache
import kotlin.system.exitProcess import kotlin.system.exitProcess
@@ -25,16 +26,28 @@ class MainActivity : ComponentActivity() {
private val profileCache by lazy { ProfileCache(NostrManager.instance) } private val profileCache by lazy { ProfileCache(NostrManager.instance) }
private val scope = MainScope() private val scope = MainScope()
private val settingsRepository by lazy {
val storage = AppStore(this@MainActivity)
SettingsRepository(storage, scope)
}
private val accountRepository by lazy { private val accountRepository by lazy {
val storage = AppStore(this@MainActivity) val storage = AppStore(this@MainActivity)
val mediaRepository = MediaRepository() val mediaRepository = MediaRepository(settingsRepository)
val androidSigner = AndroidExternalSigner(this@MainActivity, externalSignerLauncher) val androidSigner = AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
AccountRepository(NostrManager.instance, storage, mediaRepository, scope, androidSigner) AccountRepository(
NostrManager.instance,
storage,
mediaRepository,
settingsRepository,
scope,
androidSigner
)
} }
private val chatRepository by lazy { private val chatRepository by lazy {
val mediaRepository = MediaRepository() val mediaRepository = MediaRepository(settingsRepository)
ChatRepository(NostrManager.instance, mediaRepository, scope) ChatRepository(NostrManager.instance, mediaRepository, settingsRepository, scope)
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -82,6 +95,7 @@ class MainActivity : ComponentActivity() {
profileCache = profileCache, profileCache = profileCache,
accountRepository = accountRepository, accountRepository = accountRepository,
chatRepository = chatRepository, chatRepository = chatRepository,
settingsRepository = settingsRepository,
) )
} }
} }

View File

@@ -0,0 +1,22 @@
package su.reya.coop
import kotlinx.serialization.Serializable
@Serializable
data class Settings(
val theme: Theme = Theme.System,
val dynamicColor: Boolean = true,
val media: Media = Media.AlwaysEnabled,
val screening: Boolean = true,
val blossomServer: String? = "https://blossom.band",
)
@Serializable
enum class Theme {
Light, Dark, System
}
@Serializable
enum class Media {
Disabled, DisabledForMobileData, AlwaysEnabled
}

View File

@@ -57,6 +57,7 @@ class AccountRepository(
private val nostr: Nostr, private val nostr: Nostr,
private val storage: AppStorage, private val storage: AppStorage,
private val mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
private val settingsRepository: SettingsRepository,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val externalSignerHandler: ExternalSignerHandler? = null, private val externalSignerHandler: ExternalSignerHandler? = null,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
@@ -75,9 +76,7 @@ class AccountRepository(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val currentUserProfile: StateFlow<Profile?> = nostr.signer.publicKeyFlow val currentUserProfile: StateFlow<Profile?> = nostr.signer.publicKeyFlow
.flatMapLatest { pubkey -> .flatMapLatest { if (it != null) currentUserProfileFlow(it) else flowOf(null) }
if (pubkey != null) currentUserProfileFlow(pubkey) else flowOf(null)
}
.stateIn(scope, SharingStarted.WhileSubscribed(5000), null) .stateIn(scope, SharingStarted.WhileSubscribed(5000), null)
init { init {

View File

@@ -36,6 +36,7 @@ data class ChatState(
class ChatRepository( class ChatRepository(
private val nostr: Nostr, private val nostr: Nostr,
private val mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
private val settingsRepository: SettingsRepository,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
) : ErrorHost by createErrorHost() { ) : ErrorHost by createErrorHost() {

View File

@@ -8,7 +8,9 @@ import kotlinx.serialization.json.Json
import rust.nostr.sdk.AsyncNostrSigner import rust.nostr.sdk.AsyncNostrSigner
import su.reya.coop.blossom.BlossomClient import su.reya.coop.blossom.BlossomClient
class MediaRepository { class MediaRepository(
private val settingsRepository: SettingsRepository
) {
private val httpClient = HttpClient { private val httpClient = HttpClient {
install(ContentNegotiation) { install(ContentNegotiation) {
json(Json { json(Json {
@@ -25,12 +27,9 @@ class MediaRepository {
contentType: String? = "image/jpeg" contentType: String? = "image/jpeg"
): String? { ): String? {
return try { return try {
val blossom = BlossomClient(url = "https://blossom.band", client = httpClient) val url = settingsRepository.settings.value.blossomServer ?: "https://blossom.band"
val descriptor = blossom.upload( val blossom = BlossomClient(url, httpClient)
file = file, val descriptor = blossom.upload(file = file, contentType = contentType, signer = signer)
contentType = contentType,
signer = signer,
)
descriptor?.url descriptor?.url
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e

View File

@@ -0,0 +1,61 @@
package su.reya.coop.repository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import su.reya.coop.AppStorage
import su.reya.coop.Settings
class SettingsRepository(
private val storage: AppStorage,
private val scope: CoroutineScope
) {
companion object {
private const val KEY_SETTINGS = "app_settings"
}
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
private val _settings = MutableStateFlow(Settings())
val settings: StateFlow<Settings> = _settings.asStateFlow()
init {
scope.launch {
_settings.value = load()
}
}
fun update(transform: (Settings) -> Settings) {
scope.launch {
val newSettings = transform(_settings.value)
_settings.value = newSettings
save(newSettings)
}
}
private suspend fun save(settings: Settings) {
val jsonString = json.encodeToString(settings)
storage.set(KEY_SETTINGS, jsonString)
}
private suspend fun load(): Settings {
val jsonString = storage.get(KEY_SETTINGS)
return if (jsonString != null) {
try {
json.decodeFromString<Settings>(jsonString)
} catch (_: Exception) {
Settings()
}
} else {
Settings()
}
}
}

View File

@@ -0,0 +1,16 @@
package su.reya.coop.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.StateFlow
import su.reya.coop.Settings
import su.reya.coop.repository.SettingsRepository
class SettingsViewModel(
private val repository: SettingsRepository
) : ViewModel() {
val settings: StateFlow<Settings> = repository.settings
fun update(transform: (Settings) -> Settings) {
repository.update(transform)
}
}