Compare commits

...

3 Commits

Author SHA1 Message Date
PTah 6b4020adf1 fix: краш «Свой звук» на FragmentActivity (0.5.5)
Ringtone picker через startActivityForResult вместо Activity Result API с большим requestCode.
2026-06-14 18:32:44 +10:00
PTah f41a0d3a50 feat: настройка звука push-уведомлений (0.5.4)
Единый звук для каналов, выбор системный/свой в Устройстве, исправлен краш ringtone picker.
2026-06-14 18:29:21 +10:00
PTah 2aaea30468 chore: dev-заглушки полей подключения для обкатки (0.5.2)
Prefill URL, логина, пароля и имени устройства только в debug-сборке.
2026-06-14 17:51:11 +10:00
13 changed files with 333 additions and 24 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "ru.kalinamall.seaca" applicationId = "ru.kalinamall.seaca"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 15 versionCode = 19
versionName = "0.5.1" versionName = "0.5.5"
} }
buildTypes { buildTypes {
@@ -59,6 +59,7 @@ import ru.kalinamall.seaca.ui.screens.HostDetailScreen
import ru.kalinamall.seaca.ui.screens.HostsScreen import ru.kalinamall.seaca.ui.screens.HostsScreen
import ru.kalinamall.seaca.ui.screens.ProblemDetailScreen import ru.kalinamall.seaca.ui.screens.ProblemDetailScreen
import ru.kalinamall.seaca.ui.screens.ProblemsScreen import ru.kalinamall.seaca.ui.screens.ProblemsScreen
import ru.kalinamall.seaca.ui.RingtonePicker
import ru.kalinamall.seaca.ui.screens.ReportsScreen import ru.kalinamall.seaca.ui.screens.ReportsScreen
import ru.kalinamall.seaca.ui.theme.SeacaTheme import ru.kalinamall.seaca.ui.theme.SeacaTheme
@@ -94,6 +95,13 @@ class MainActivity : FragmentActivity() {
deepLinkState.value = extractDeepLink(intent) deepLinkState.value = extractDeepLink(intent)
} }
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
RingtonePicker.handleActivityResult(requestCode, resultCode, data)
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
if (shouldRequireBiometric()) { if (shouldRequireBiometric()) {
@@ -3,9 +3,11 @@ package ru.kalinamall.seaca
import android.app.Application import android.app.Application
import ru.kalinamall.seaca.push.NotificationHelper import ru.kalinamall.seaca.push.NotificationHelper
import ru.kalinamall.seaca.push.NotificationSoundResolver
class SeacaApplication : Application() { class SeacaApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
NotificationHelper.ensureChannels(this) NotificationHelper.ensureChannels(this, NotificationSoundResolver.defaultNotificationUri())
} }
} }
@@ -0,0 +1,20 @@
package ru.kalinamall.seaca.data
enum class NotificationSoundSource(
val storageKey: String,
val label: String,
) {
SYSTEM("system", "Системный звук"),
CUSTOM("custom", "Свой звук"),
;
companion object {
fun fromStorage(value: String?): NotificationSoundSource =
entries.firstOrNull { it.storageKey == value } ?: SYSTEM
}
}
data class NotificationSoundSettings(
val source: NotificationSoundSource,
val customUri: String?,
)
@@ -40,12 +40,27 @@ class SessionStore(private val context: Context) {
DateTimeDisplayMode.fromStorage(prefs[KEY_DATE_TIME_FORMAT]) DateTimeDisplayMode.fromStorage(prefs[KEY_DATE_TIME_FORMAT])
} }
val notificationSoundSettings: Flow<NotificationSoundSettings> = context.dataStore.data.map { prefs ->
NotificationSoundSettings(
source = NotificationSoundSource.fromStorage(prefs[KEY_NOTIFICATION_SOUND_SOURCE]),
customUri = prefs[KEY_NOTIFICATION_SOUND_URI],
)
}
suspend fun getNotificationMode(): NotificationMode = suspend fun getNotificationMode(): NotificationMode =
NotificationMode.fromStorage(context.dataStore.data.first()[KEY_NOTIFICATION_MODE]) NotificationMode.fromStorage(context.dataStore.data.first()[KEY_NOTIFICATION_MODE])
suspend fun getDateTimeDisplayMode(): DateTimeDisplayMode = suspend fun getDateTimeDisplayMode(): DateTimeDisplayMode =
DateTimeDisplayMode.fromStorage(context.dataStore.data.first()[KEY_DATE_TIME_FORMAT]) DateTimeDisplayMode.fromStorage(context.dataStore.data.first()[KEY_DATE_TIME_FORMAT])
suspend fun getNotificationSoundSettings(): NotificationSoundSettings {
val prefs = context.dataStore.data.first()
return NotificationSoundSettings(
source = NotificationSoundSource.fromStorage(prefs[KEY_NOTIFICATION_SOUND_SOURCE]),
customUri = prefs[KEY_NOTIFICATION_SOUND_URI],
)
}
suspend fun setNotificationMode(mode: NotificationMode) { suspend fun setNotificationMode(mode: NotificationMode) {
context.dataStore.edit { it[KEY_NOTIFICATION_MODE] = mode.storageKey } context.dataStore.edit { it[KEY_NOTIFICATION_MODE] = mode.storageKey }
} }
@@ -54,6 +69,17 @@ class SessionStore(private val context: Context) {
context.dataStore.edit { it[KEY_DATE_TIME_FORMAT] = mode.storageKey } context.dataStore.edit { it[KEY_DATE_TIME_FORMAT] = mode.storageKey }
} }
suspend fun setNotificationSound(source: NotificationSoundSource, customUri: String?) {
context.dataStore.edit {
it[KEY_NOTIFICATION_SOUND_SOURCE] = source.storageKey
if (customUri.isNullOrBlank()) {
it.remove(KEY_NOTIFICATION_SOUND_URI)
} else {
it[KEY_NOTIFICATION_SOUND_URI] = customUri
}
}
}
suspend fun getBaseUrl(): String? = context.dataStore.data.first()[KEY_BASE_URL] suspend fun getBaseUrl(): String? = context.dataStore.data.first()[KEY_BASE_URL]
suspend fun trustAllCerts(): Boolean = context.dataStore.data.first()[KEY_TRUST_ALL] ?: false suspend fun trustAllCerts(): Boolean = context.dataStore.data.first()[KEY_TRUST_ALL] ?: false
@@ -114,6 +140,8 @@ class SessionStore(private val context: Context) {
private val KEY_TRUST_ALL = booleanPreferencesKey("trust_all_certs") private val KEY_TRUST_ALL = booleanPreferencesKey("trust_all_certs")
private val KEY_NOTIFICATION_MODE = stringPreferencesKey("notification_mode") private val KEY_NOTIFICATION_MODE = stringPreferencesKey("notification_mode")
private val KEY_DATE_TIME_FORMAT = stringPreferencesKey("date_time_format") private val KEY_DATE_TIME_FORMAT = stringPreferencesKey("date_time_format")
private val KEY_NOTIFICATION_SOUND_SOURCE = stringPreferencesKey("notification_sound_source")
private val KEY_NOTIFICATION_SOUND_URI = stringPreferencesKey("notification_sound_uri")
private const val KEY_ACCESS = "access_token" private const val KEY_ACCESS = "access_token"
private const val KEY_REFRESH = "refresh_token" private const val KEY_REFRESH = "refresh_token"
private const val KEY_DEVICE_ID = "device_id" private const val KEY_DEVICE_ID = "device_id"
@@ -5,6 +5,8 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import ru.kalinamall.seaca.MainActivity import ru.kalinamall.seaca.MainActivity
@@ -18,35 +20,47 @@ object NotificationHelper {
const val CHANNEL_HIGH_SILENT = "sac_high_silent" const val CHANNEL_HIGH_SILENT = "sac_high_silent"
const val CHANNEL_CRITICAL_SILENT = "sac_critical_silent" const val CHANNEL_CRITICAL_SILENT = "sac_critical_silent"
fun ensureChannels(context: Context) { private val soundChannelIds = listOf(CHANNEL_DEFAULT, CHANNEL_HIGH, CHANNEL_CRITICAL)
private var appliedSoundKey: String? = null
fun ensureChannels(context: Context, soundUri: Uri = NotificationSoundResolver.defaultNotificationUri()) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val soundKey = soundUri.toString()
val mgr = context.getSystemService(NotificationManager::class.java) val mgr = context.getSystemService(NotificationManager::class.java)
if (soundKey != appliedSoundKey) {
soundChannelIds.forEach { mgr.deleteNotificationChannel(it) }
appliedSoundKey = soundKey
}
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
mgr.createNotificationChannel( mgr.createNotificationChannel(
NotificationChannel( soundChannel(context, CHANNEL_DEFAULT, R.string.channel_default, NotificationManager.IMPORTANCE_DEFAULT, soundUri, audioAttributes),
CHANNEL_DEFAULT,
context.getString(R.string.channel_default),
NotificationManager.IMPORTANCE_DEFAULT,
),
) )
mgr.createNotificationChannel( mgr.createNotificationChannel(
NotificationChannel( soundChannel(context, CHANNEL_HIGH, R.string.channel_high, NotificationManager.IMPORTANCE_HIGH, soundUri, audioAttributes),
CHANNEL_HIGH,
context.getString(R.string.channel_high),
NotificationManager.IMPORTANCE_HIGH,
),
) )
mgr.createNotificationChannel( mgr.createNotificationChannel(
NotificationChannel( soundChannel(context, CHANNEL_CRITICAL, R.string.channel_critical, NotificationManager.IMPORTANCE_HIGH, soundUri, audioAttributes),
CHANNEL_CRITICAL,
context.getString(R.string.channel_critical),
NotificationManager.IMPORTANCE_HIGH,
),
) )
mgr.createNotificationChannel(silentChannel(context, CHANNEL_DEFAULT_SILENT, R.string.channel_default_silent)) mgr.createNotificationChannel(silentChannel(context, CHANNEL_DEFAULT_SILENT, R.string.channel_default_silent))
mgr.createNotificationChannel(silentChannel(context, CHANNEL_HIGH_SILENT, R.string.channel_high_silent)) mgr.createNotificationChannel(silentChannel(context, CHANNEL_HIGH_SILENT, R.string.channel_high_silent))
mgr.createNotificationChannel(silentChannel(context, CHANNEL_CRITICAL_SILENT, R.string.channel_critical_silent)) mgr.createNotificationChannel(silentChannel(context, CHANNEL_CRITICAL_SILENT, R.string.channel_critical_silent))
} }
private fun soundChannel(
context: Context,
id: String,
nameRes: Int,
importance: Int,
soundUri: Uri,
audioAttributes: AudioAttributes,
): NotificationChannel = NotificationChannel(id, context.getString(nameRes), importance).apply {
setSound(soundUri, audioAttributes)
enableVibration(true)
}
private fun silentChannel(context: Context, id: String, nameRes: Int): NotificationChannel = private fun silentChannel(context: Context, id: String, nameRes: Int): NotificationChannel =
NotificationChannel(id, context.getString(nameRes), NotificationManager.IMPORTANCE_DEFAULT).apply { NotificationChannel(id, context.getString(nameRes), NotificationManager.IMPORTANCE_DEFAULT).apply {
setSound(null, null) setSound(null, null)
@@ -66,8 +80,9 @@ object NotificationHelper {
kind: String?, kind: String?,
id: String?, id: String?,
silent: Boolean = false, silent: Boolean = false,
soundUri: Uri = NotificationSoundResolver.defaultNotificationUri(),
) { ) {
ensureChannels(context) ensureChannels(context, soundUri)
val channel = resolveChannel(severity, silent) val channel = resolveChannel(severity, silent)
val intent = Intent(context, MainActivity::class.java).apply { val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@@ -0,0 +1,30 @@
package ru.kalinamall.seaca.push
import android.content.Context
import android.media.RingtoneManager
import android.net.Uri
import ru.kalinamall.seaca.data.NotificationSoundSettings
import ru.kalinamall.seaca.data.NotificationSoundSource
object NotificationSoundResolver {
fun resolve(context: Context, settings: NotificationSoundSettings): Uri {
return when (settings.source) {
NotificationSoundSource.SYSTEM -> defaultNotificationUri()
NotificationSoundSource.CUSTOM -> {
settings.customUri?.let { Uri.parse(it) } ?: defaultNotificationUri()
}
}
}
fun defaultNotificationUri(): Uri =
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
fun displayName(context: Context, settings: NotificationSoundSettings): String {
return try {
val uri = resolve(context, settings)
RingtoneManager.getRingtone(context, uri)?.getTitle(context) ?: ""
} catch (_: Exception) {
""
}
}
}
@@ -27,6 +27,12 @@ class SeacaMessagingService : FirebaseMessagingService() {
val data = message.data val data = message.data
val title = data["title"] ?: message.notification?.title ?: "SAC" val title = data["title"] ?: message.notification?.title ?: "SAC"
val body = data["body"] ?: message.notification?.body ?: "" val body = data["body"] ?: message.notification?.body ?: ""
val soundUri = if (silent) {
NotificationSoundResolver.defaultNotificationUri()
} else {
val settings = runBlocking { repo.session.getNotificationSoundSettings() }
NotificationSoundResolver.resolve(this, settings)
}
NotificationHelper.show( NotificationHelper.show(
context = this, context = this,
title = title, title = title,
@@ -35,6 +41,7 @@ class SeacaMessagingService : FirebaseMessagingService() {
kind = data["kind"], kind = data["kind"],
id = data["id"], id = data["id"],
silent = silent, silent = silent,
soundUri = soundUri,
) )
} }
@@ -0,0 +1,52 @@
package ru.kalinamall.seaca.ui
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import androidx.fragment.app.FragmentActivity
import ru.kalinamall.seaca.push.NotificationSoundResolver
object RingtonePicker {
private const val REQUEST_CODE = 9001
private var callback: ((Uri?) -> Unit)? = null
fun launch(activity: FragmentActivity, currentUri: Uri, onResult: (Uri?) -> Unit) {
callback = onResult
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, NotificationSoundResolver.defaultNotificationUri())
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentUri)
}
try {
@Suppress("DEPRECATION")
activity.startActivityForResult(intent, REQUEST_CODE)
} catch (_: ActivityNotFoundException) {
callback = null
throw ActivityNotFoundException()
}
}
fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != REQUEST_CODE) return
val onResult = callback ?: return
callback = null
val uri = if (resultCode == Activity.RESULT_OK) readResult(data) else null
onResult(uri)
}
private fun readResult(data: Intent?): Uri? {
if (data == null) return null
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java)
} else {
@Suppress("DEPRECATION")
data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
}
}
@@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.kalinamall.seaca.BuildConfig
import ru.kalinamall.seaca.data.SacRepository import ru.kalinamall.seaca.data.SacRepository
@Composable @Composable
@@ -29,11 +30,20 @@ fun ConnectScreen(
repo: SacRepository, repo: SacRepository,
onConnected: () -> Unit, onConnected: () -> Unit,
) { ) {
var serverUrl by remember { mutableStateOf("") } val devDefaults = BuildConfig.DEBUG
var serverUrl by remember {
mutableStateOf(if (devDefaults) DevConnectDefaults.SERVER_URL else "")
}
var enrollmentCode by remember { mutableStateOf("") } var enrollmentCode by remember { mutableStateOf("") }
var username by remember { mutableStateOf("") } var username by remember {
var password by remember { mutableStateOf("") } mutableStateOf(if (devDefaults) DevConnectDefaults.USERNAME else "")
var displayName by remember { mutableStateOf(android.os.Build.MODEL) } }
var password by remember {
mutableStateOf(if (devDefaults) DevConnectDefaults.PASSWORD else "")
}
var displayName by remember {
mutableStateOf(if (devDefaults) DevConnectDefaults.DISPLAY_NAME else android.os.Build.MODEL)
}
var trustCert by remember { mutableStateOf(false) } var trustCert by remember { mutableStateOf(false) }
var loading by remember { mutableStateOf(false) } var loading by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
@@ -1,5 +1,10 @@
package ru.kalinamall.seaca.ui.screens package ru.kalinamall.seaca.ui.screens
import android.content.ActivityNotFoundException
import android.widget.Toast
import androidx.compose.runtime.rememberUpdatedState
import androidx.fragment.app.FragmentActivity
import android.media.RingtoneManager
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -35,6 +40,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -46,8 +52,13 @@ import ru.kalinamall.seaca.data.HostDetail
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ru.kalinamall.seaca.data.DateTimeDisplayMode import ru.kalinamall.seaca.data.DateTimeDisplayMode
import ru.kalinamall.seaca.data.NotificationMode import ru.kalinamall.seaca.data.NotificationMode
import ru.kalinamall.seaca.data.NotificationSoundSettings
import ru.kalinamall.seaca.data.NotificationSoundSource
import ru.kalinamall.seaca.data.ProblemDetail import ru.kalinamall.seaca.data.ProblemDetail
import ru.kalinamall.seaca.data.SacRepository import ru.kalinamall.seaca.data.SacRepository
import ru.kalinamall.seaca.push.NotificationHelper
import ru.kalinamall.seaca.push.NotificationSoundResolver
import ru.kalinamall.seaca.ui.RingtonePicker
import ru.kalinamall.seaca.ui.components.BackArrowButton import ru.kalinamall.seaca.ui.components.BackArrowButton
import ru.kalinamall.seaca.ui.components.ForwardArrowButton import ru.kalinamall.seaca.ui.components.ForwardArrowButton
import ru.kalinamall.seaca.ui.util.SacDateTimes import ru.kalinamall.seaca.ui.util.SacDateTimes
@@ -418,11 +429,56 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
var username by remember { mutableStateOf<String?>(null) } var username by remember { mutableStateOf<String?>(null) }
var role by remember { mutableStateOf<String?>(null) } var role by remember { mutableStateOf<String?>(null) }
var deviceId by remember { mutableStateOf(0) } var deviceId by remember { mutableStateOf(0) }
val context = LocalContext.current
val notificationMode by repo.session.notificationMode.collectAsState(initial = NotificationMode.PUSH_SOUND) val notificationMode by repo.session.notificationMode.collectAsState(initial = NotificationMode.PUSH_SOUND)
val soundSettings by repo.session.notificationSoundSettings.collectAsStateWithLifecycle(
initialValue = NotificationSoundSettings(NotificationSoundSource.SYSTEM, null),
)
val dateTimeMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle( val dateTimeMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH, initialValue = DateTimeDisplayMode.SLASH,
) )
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val activity = context as? FragmentActivity
val latestSoundSettings by rememberUpdatedState(soundSettings)
suspend fun applySoundChannels(settings: NotificationSoundSettings) {
NotificationHelper.ensureChannels(
context,
NotificationSoundResolver.resolve(context, settings),
)
}
LaunchedEffect(soundSettings) {
if (notificationMode == NotificationMode.PUSH_SOUND) {
applySoundChannels(soundSettings)
}
}
fun openRingtonePicker() {
if (activity == null) {
Toast.makeText(context, "Не удалось открыть выбор звука", Toast.LENGTH_SHORT).show()
return
}
val currentUri = NotificationSoundResolver.resolve(context, latestSoundSettings)
try {
RingtonePicker.launch(activity, currentUri) { uri ->
scope.launch {
if (uri == null) {
repo.session.setNotificationSound(NotificationSoundSource.SYSTEM, null)
} else {
repo.session.setNotificationSound(NotificationSoundSource.CUSTOM, uri.toString())
}
applySoundChannels(repo.session.getNotificationSoundSettings())
}
}
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, "На устройстве нет выбора звука уведомлений", Toast.LENGTH_LONG).show()
}
}
fun previewSound() {
RingtoneManager.getRingtone(context, NotificationSoundResolver.resolve(context, soundSettings))?.play()
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
baseUrl = repo.session.getBaseUrl() baseUrl = repo.session.getBaseUrl()
@@ -482,6 +538,71 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
} }
} }
if (notificationMode == NotificationMode.PUSH_SOUND) {
Text(
stringResource(R.string.notification_sound_title),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 8.dp),
)
NotificationSoundSource.entries.forEach { source ->
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
selected = soundSettings.source == source,
onClick = {
when (source) {
NotificationSoundSource.SYSTEM -> {
scope.launch {
repo.session.setNotificationSound(source, null)
applySoundChannels(repo.session.getNotificationSoundSettings())
}
}
NotificationSoundSource.CUSTOM -> {
if (soundSettings.customUri.isNullOrBlank()) {
openRingtonePicker()
} else {
scope.launch {
repo.session.setNotificationSound(source, soundSettings.customUri)
applySoundChannels(repo.session.getNotificationSoundSettings())
}
}
}
}
},
)
Text(
source.label,
modifier = Modifier.padding(start = 8.dp),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
Text(
stringResource(
R.string.notification_sound_current,
NotificationSoundResolver.displayName(context, soundSettings),
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (soundSettings.source == NotificationSoundSource.CUSTOM) {
Button(onClick = { openRingtonePicker() }) {
Text(stringResource(R.string.notification_sound_pick))
}
}
Button(onClick = { previewSound() }) {
Text(stringResource(R.string.notification_sound_preview))
}
}
}
Text( Text(
stringResource(R.string.date_time_format_title), stringResource(R.string.date_time_format_title),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@@ -0,0 +1,12 @@
package ru.kalinamall.seaca.ui.screens
/**
* Временные значения для обкатки. Удалить перед релизом.
* Используются только в debug-сборке ([ru.kalinamall.seaca.BuildConfig.DEBUG]).
*/
internal object DevConnectDefaults {
const val SERVER_URL = "https://sac-api.kalinamall.ru"
const val USERNAME = "admin"
const val PASSWORD = "R3tt3tnatur"
const val DISPLAY_NAME = "papas s25"
}
+4
View File
@@ -9,4 +9,8 @@
<string name="channel_critical_silent">SAC: критические (без звука)</string> <string name="channel_critical_silent">SAC: критические (без звука)</string>
<string name="notification_mode_title">Push-уведомления</string> <string name="notification_mode_title">Push-уведомления</string>
<string name="date_time_format_title">Формат даты и времени</string> <string name="date_time_format_title">Формат даты и времени</string>
<string name="notification_sound_title">Звук уведомлений</string>
<string name="notification_sound_pick">Выбрать звук</string>
<string name="notification_sound_preview">Проверить звук</string>
<string name="notification_sound_current">Сейчас: %1$s</string>
</resources> </resources>