From 66a392161c49f0a4d2cd77e0d30cffc85c1f529f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=87=E9=87=8C=E6=97=A0=E4=BA=91?= <59728718+hovthen@users.noreply.github.com> Date: Sun, 21 Apr 2024 01:17:55 +0800 Subject: [PATCH 1/7] Update Chinese(Simplified) Translation (#63) --- app/src/main/res/values-zh-rCN/strings.xml | 35 ++++++++++++++-------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6a605a2..bd11382 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,10 +1,8 @@ + - Mauth - 输入您的 PIN - 身份认证 + 生物认证 取消 - 空空如也 加载账户出错 添加新账户 @@ -20,10 +18,12 @@ 已复制 复制到剪切板失败 解析二维码失败 - 用户名 - 发行方 - 日期 - + 用户名 (A-Z) + 用户名 (Z-A) + 发行方 (A-Z) + 发行方 (Z-A) + 旧日期优先 + 新日期优先 扫描二维码 权限未授予 未授予相机权限 @@ -31,7 +31,6 @@ 去授权 下次一定 未授予所需的权限 - 添加新账户 编辑账户 保存 @@ -50,8 +49,8 @@ 加载账户时出错 此处为必填项 允许范围从 %1$s 至 %2$s - 设置 + 安全 截屏保护 阻止应用内截图并模糊后台预览图 PIN 密码锁 @@ -61,12 +60,22 @@ 取消 禁用生物认证 取消 - + 外观 + 主题 设置您的 PIN 确认您的 PIN - 输入您的 PIN - + 主题 + 跟随系统 + 暗色 + 浅色 + 动态 + 罗兰紫 + 宝石蓝 + 咸蛋黄 + 鲜苔绿 + 果粒橙 + 海松青 关于 应用版本 v%1$s 开源仓库 From 994c336542afd68238e47d675756e160a14f3bfc Mon Sep 17 00:00:00 2001 From: Yurt Page Date: Sat, 27 Apr 2024 11:42:01 +0300 Subject: [PATCH 2/7] Add Russian translation (#64) Co-authored-by: Tornike Khintibidze <48173186+X1nto@users.noreply.github.com> --- app/src/main/res/values-ru/strings.xml | 90 +++++++++++++++++++ .../metadata/android/ru/full_description.txt | 11 +++ .../metadata/android/ru/short_description.txt | 1 + 3 files changed, 102 insertions(+) create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 fastlane/metadata/android/ru/full_description.txt create mode 100644 fastlane/metadata/android/ru/short_description.txt diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..1a59fd3 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,90 @@ + + Введите ваш PIN + Биометрическая Аутентификация + Отмена + + Тут ничего нет + Не удалось загрузить учётные записи + Добавить учётную запись + Отсканируйте QR код или введите ключ вручную + Сканировать QR код + Выберете изображение + Ввести данные вручную + Удалить учётные записи? + Выбранные учётные записи будут навсегда удалены. + Удалить + Отмена + Настройки + Код скопирован в буфер обмена + Не удалось скопировать код + Не удалось разобрать QR код + Метка (A-Z) + Метка (Z-A) + Эмитент (A-Z) + Эмитент (Z-A) + Со старых + С новых + + Сканировать QR код + Не хватает разрешений + Разрешение на камеру не предоставлено. + Разрешение на камеру требуется, чтобы это приложение сканировало и анализировала QR код, без него Mauth не может сканировать и импортировать ваши учетные записи. + Разрешить + Отмена + Не хватает разрешений + + Добавить учётную запись + Изменить учётную запись + Сохранить + Метка + Эмитент + Секрет + Тип + Алгоритм + Цифры + Счётчик + Период + Отменить изменения? + Ваши изменения не будут сохранены. + Discard + Отмена + Произошла ошибка при загрузке учетной записи + требуется + в диапазоне %1$s-%2$s + + Settings + Безопасность + Режим безопасности + Предотвращать скриншоты в приложении + Блокировка PIN кодом + Требовать аутентификация при старте + Биометрическая аутентификация + Установить биометрию + Отмена + Отключить биометрию + Отмена + Внешний вид + Тема + + Установить PIN + Подтвердите ваш PIN + + Введите ваш PIN + + Тема + Система + Тёмная + Светлая + Динамический + Moth Purple + Blueberry Blue + Pickle Yellow + Toxic Green + Leather Orange + Ocean Turquoise + + О приложении + Версия v%1$s + Исходники + Обратная связь + diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt new file mode 100644 index 0000000..315027d --- /dev/null +++ b/fastlane/metadata/android/ru/full_description.txt @@ -0,0 +1,11 @@ +Mauth (произносится как моз) это двухфакторное приложение для аутентификации с поддержкой TOTP и HOTP и совместимое с Google Authenticator. Оно имеет красивый интерфейс, а также предоставляет много необходимых функций. + +Возможности: +- Совместимо с Google Authenticator +- Безопасное +- Множество способов добавить свои учетные записи +- Алгоритмы +- Организация +- Экспорт/импорт + +Mauth использует новейшие технологии Android (JetPack Compose, Camerax, Biometrics, Room DB и другие) чтобы обеспечить наилучший пользовательский опыт с наименьшим количеством ошибок. Оно находится в активной разработке, но ещё не готово к ежедневному использованию. Используйте его на свой страх и риск. \ No newline at end of file diff --git a/fastlane/metadata/android/ru/short_description.txt b/fastlane/metadata/android/ru/short_description.txt new file mode 100644 index 0000000..985d5e0 --- /dev/null +++ b/fastlane/metadata/android/ru/short_description.txt @@ -0,0 +1 @@ +Приложение для двухфакторной аутентификации в стиле Material You \ No newline at end of file From c8df53d9fef74d6c0afa33476e8463ed938190cf Mon Sep 17 00:00:00 2001 From: Xinto Date: Sun, 28 Apr 2024 12:13:42 +0400 Subject: [PATCH 3/7] Update the account screen to copy the custom icon --- .../contracts/PickVisualMediaPersistent.kt | 16 -- .../mauth/domain/account/AccountRepository.kt | 4 +- .../domain/account/model/DomainAccountInfo.kt | 33 ++-- .../xinto/mauth/domain/otp/OtpRepository.kt | 7 +- .../java/com/xinto/mauth/ui/MainActivity.kt | 5 +- .../mauth/ui/screen/account/AccountScreen.kt | 99 ++++++---- .../ui/screen/account/AccountViewModel.kt | 181 ++++++++++++++++-- .../account/state/AccountScreenSuccess.kt | 78 ++++---- 8 files changed, 278 insertions(+), 145 deletions(-) delete mode 100644 app/src/main/java/com/xinto/mauth/core/contracts/PickVisualMediaPersistent.kt diff --git a/app/src/main/java/com/xinto/mauth/core/contracts/PickVisualMediaPersistent.kt b/app/src/main/java/com/xinto/mauth/core/contracts/PickVisualMediaPersistent.kt deleted file mode 100644 index 6dbcd94..0000000 --- a/app/src/main/java/com/xinto/mauth/core/contracts/PickVisualMediaPersistent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.xinto.mauth.core.contracts - -import android.content.Context -import android.content.Intent -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts - -class PickVisualMediaPersistent : ActivityResultContracts.PickVisualMedia() { - - override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent { - return super.createIntent(context, input).apply { - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/xinto/mauth/domain/account/AccountRepository.kt b/app/src/main/java/com/xinto/mauth/domain/account/AccountRepository.kt index 1ba4ebc..8ce2d80 100644 --- a/app/src/main/java/com/xinto/mauth/domain/account/AccountRepository.kt +++ b/app/src/main/java/com/xinto/mauth/domain/account/AccountRepository.kt @@ -113,7 +113,7 @@ class AccountRepository( private fun DomainAccountInfo.toEntityAccount(): EntityAccount { return EntityAccount( - id = id ?: UUID.randomUUID(), + id = id, icon = icon, secret = secret, label = label, @@ -122,7 +122,7 @@ class AccountRepository( type = type, digits = digits.toInt(), period = period.toInt(), - createDateMillis = createdMillis ?: System.currentTimeMillis() + createDateMillis = createdMillis ) } diff --git a/app/src/main/java/com/xinto/mauth/domain/account/model/DomainAccountInfo.kt b/app/src/main/java/com/xinto/mauth/domain/account/model/DomainAccountInfo.kt index 3e929f8..166dd77 100644 --- a/app/src/main/java/com/xinto/mauth/domain/account/model/DomainAccountInfo.kt +++ b/app/src/main/java/com/xinto/mauth/domain/account/model/DomainAccountInfo.kt @@ -11,7 +11,7 @@ import java.util.UUID @Immutable @Parcelize data class DomainAccountInfo( - val id: UUID?, + val id: UUID, val icon: Uri?, val label: String, val issuer: String, @@ -21,7 +21,7 @@ data class DomainAccountInfo( val digits: String, val counter: String, val period: String, - val createdMillis: Long? + val createdMillis: Long ) : Parcelable { fun isValid(): Boolean { @@ -34,19 +34,20 @@ data class DomainAccountInfo( } companion object { - val DEFAULT = DomainAccountInfo( - id = null, - icon = null, - label = "", - issuer = "", - secret = "", - algorithm = OtpDigest.SHA1, - type = OtpType.TOTP, - digits = "6", - counter = "0", - period = "30", - createdMillis = null - ) + fun new(): DomainAccountInfo { + return DomainAccountInfo( + id = UUID.randomUUID(), + icon = null, + label = "", + issuer = "", + secret = "", + algorithm = OtpDigest.SHA1, + type = OtpType.TOTP, + digits = "6", + counter = "0", + period = "30", + createdMillis = System.currentTimeMillis() + ) + } } - } \ No newline at end of file diff --git a/app/src/main/java/com/xinto/mauth/domain/otp/OtpRepository.kt b/app/src/main/java/com/xinto/mauth/domain/otp/OtpRepository.kt index 2386338..dd69348 100644 --- a/app/src/main/java/com/xinto/mauth/domain/otp/OtpRepository.kt +++ b/app/src/main/java/com/xinto/mauth/domain/otp/OtpRepository.kt @@ -76,15 +76,16 @@ class OtpRepository( fun parseUriToAccountInfo(uri: String): DomainAccountInfo? { return when (val parseResult = otpUriParser.parseOtpUri(uri)) { is OtpUriParserResult.Success -> { - DomainAccountInfo.DEFAULT.copy( + val default = DomainAccountInfo.new() + default.copy( label = parseResult.data.label, issuer = parseResult.data.issuer, secret = parseResult.data.secret, algorithm = parseResult.data.algorithm, type = parseResult.data.type, digits = parseResult.data.digits.toString(), - counter = parseResult.data.counter?.toString() ?: DomainAccountInfo.DEFAULT.counter, - period = parseResult.data.period?.toString() ?: DomainAccountInfo.DEFAULT.period, + counter = parseResult.data.counter?.toString() ?: default.counter, + period = parseResult.data.period?.toString() ?: default.period, ) } is OtpUriParserResult.Failure -> null diff --git a/app/src/main/java/com/xinto/mauth/ui/MainActivity.kt b/app/src/main/java/com/xinto/mauth/ui/MainActivity.kt index 008effe..9d6a240 100644 --- a/app/src/main/java/com/xinto/mauth/ui/MainActivity.kt +++ b/app/src/main/java/com/xinto/mauth/ui/MainActivity.kt @@ -174,8 +174,9 @@ class MainActivity : FragmentActivity() { is MauthDestination.Home -> { HomeScreen( onAddAccountManually = { - navigator.navigate(MauthDestination.AddAccount( - DomainAccountInfo.DEFAULT)) + navigator.navigate( + MauthDestination.AddAccount(DomainAccountInfo.new()) + ) }, onAddAccountViaScanning = { navigator.navigate(MauthDestination.QrScanner) diff --git a/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountScreen.kt b/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountScreen.kt index 51d7151..6ae2f25 100644 --- a/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountScreen.kt +++ b/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountScreen.kt @@ -1,5 +1,6 @@ package com.xinto.mauth.ui.screen.account +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -12,11 +13,9 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -24,6 +23,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.xinto.mauth.R +import com.xinto.mauth.core.otp.model.OtpDigest +import com.xinto.mauth.core.otp.model.OtpType import com.xinto.mauth.domain.account.model.DomainAccountInfo import com.xinto.mauth.ui.screen.account.component.AccountExitDialog import com.xinto.mauth.ui.screen.account.state.AccountScreenError @@ -42,11 +43,24 @@ fun AddAccountScreen( parametersOf(AccountViewModelParams.Prefilled(prefilled)) } val state by viewModel.state.collectAsStateWithLifecycle() + val hasChanges by viewModel.hasChanges.collectAsStateWithLifecycle() + val canSave by viewModel.canSave.collectAsStateWithLifecycle() AccountScreen( title = stringResource(R.string.account_title_add), state = state, + hasChanges = hasChanges, + canSave = canSave, + onIconChange = viewModel::updateIcon, + onLabelChange = viewModel::updateLabel, + onIssuerChange = viewModel::updateIssuer, + onSecretChange = viewModel::updateSecret, + onTypeChange = viewModel::updateType, + onDigestChange = viewModel::updateDigest, + onDigitsChange = viewModel::updateDigits, + onCounterChange = viewModel::updateCounter, + onPeriodChange = viewModel::updatePeriod, onSave = { - viewModel.saveData(it) + viewModel.saveData() onExit() }, onExit = onExit @@ -62,11 +76,24 @@ fun EditAccountScreen( parametersOf(AccountViewModelParams.Id(id)) } val state by viewModel.state.collectAsStateWithLifecycle() + val hasChanges by viewModel.hasChanges.collectAsStateWithLifecycle() + val canSave by viewModel.canSave.collectAsStateWithLifecycle() AccountScreen( title = stringResource(R.string.account_title_edit), state = state, + hasChanges = hasChanges, + canSave = canSave, + onIconChange = viewModel::updateIcon, + onLabelChange = viewModel::updateLabel, + onIssuerChange = viewModel::updateIssuer, + onSecretChange = viewModel::updateSecret, + onTypeChange = viewModel::updateType, + onDigestChange = viewModel::updateDigest, + onDigitsChange = viewModel::updateDigits, + onCounterChange = viewModel::updateCounter, + onPeriodChange = viewModel::updatePeriod, onSave = { - viewModel.saveData(it) + viewModel.saveData() onExit() }, onExit = onExit @@ -77,20 +104,23 @@ fun EditAccountScreen( fun AccountScreen( title: String, state: AccountScreenState, - onSave: (DomainAccountInfo) -> Unit, + hasChanges: Boolean, + canSave: Boolean, + onIconChange: (Uri?) -> Unit, + onLabelChange: (String) -> Unit, + onIssuerChange: (String) -> Unit, + onSecretChange: (String) -> Unit, + onTypeChange: (OtpType) -> Unit, + onDigestChange: (OtpDigest) -> Unit, + onDigitsChange: (String) -> Unit, + onCounterChange: (String) -> Unit, + onPeriodChange: (String) -> Unit, + onSave: () -> Unit, onExit: () -> Unit, ) { var isExitDialogShown by remember { mutableStateOf(false) } - var accountInfo: DomainAccountInfo? by rememberSaveable { - mutableStateOf(null) - } - LaunchedEffect(state) { - if (state is AccountScreenState.Success) { - accountInfo = state.info - } - } BackHandler { - if (state is AccountScreenState.Success) { + if (hasChanges) { isExitDialogShown = true } else { onExit() @@ -102,15 +132,15 @@ fun AccountScreen( TopAppBar( actions = { TextButton( - onClick = { onSave(accountInfo!!) }, - enabled = accountInfo?.isValid() == true + onClick = onSave, + enabled = canSave ) { Text(stringResource(R.string.account_actions_save)) } }, navigationIcon = { IconButton(onClick = { - if (accountInfo != null) { + if (hasChanges) { isExitDialogShown = true } else { onExit() @@ -140,29 +170,18 @@ fun AccountScreen( AccountScreenLoading() } is AccountScreenState.Success -> { - accountInfo?.let { info -> - AccountScreenSuccess( - id = info.id, - icon = info.icon, - onIconChange = { accountInfo = info.copy(icon = it) }, - label = info.label, - onLabelChange = { accountInfo = info.copy(label = it) }, - issuer = info.issuer, - onIssuerChange = { accountInfo = info.copy(issuer = it) }, - secret = info.secret, - onSecretChange = { accountInfo = info.copy(secret = it) }, - type = info.type, - onTypeChange = { accountInfo = info.copy(type = it) }, - digest = info.algorithm, - onDigestChange = { accountInfo = info.copy(algorithm = it) }, - digits = info.digits, - onDigitsChange = { accountInfo = info.copy(digits = it) }, - counter = info.counter, - onCounterChange = { accountInfo = info.copy(counter = it) }, - period = info.period, - onPeriodChange = { accountInfo = info.copy(period = it) } - ) - } + AccountScreenSuccess( + info = state.info, + onIconChange = onIconChange, + onLabelChange = onLabelChange, + onIssuerChange = onIssuerChange, + onSecretChange = onSecretChange, + onTypeChange = onTypeChange, + onDigestChange = onDigestChange, + onDigitsChange = onDigitsChange, + onCounterChange = onCounterChange, + onPeriodChange = onPeriodChange + ) } is AccountScreenState.Error -> { AccountScreenError() diff --git a/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountViewModel.kt b/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountViewModel.kt index f004688..7bf9184 100644 --- a/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountViewModel.kt +++ b/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountViewModel.kt @@ -1,15 +1,32 @@ package com.xinto.mauth.ui.screen.account -import androidx.lifecycle.ViewModel +import android.app.Application +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import androidx.core.net.toFile +import androidx.core.net.toUri +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.xinto.mauth.core.otp.model.OtpDigest +import com.xinto.mauth.core.otp.model.OtpType import com.xinto.mauth.domain.account.AccountRepository import com.xinto.mauth.domain.account.model.DomainAccountInfo -import com.xinto.mauth.util.catchMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.io.File import java.util.UUID sealed interface AccountViewModelParams { @@ -21,31 +38,153 @@ sealed interface AccountViewModelParams { } class AccountViewModel( + application: Application, + params: AccountViewModelParams, private val accounts: AccountRepository -) : ViewModel() { - - val state = when (params) { - is AccountViewModelParams.Id -> { - accounts.getAccountInfo(params.id) - .map { - AccountScreenState.Success(it) - }.catchMap { - AccountScreenState.Error(it.localizedMessage ?: it.message ?: it.stackTraceToString()) - } - } - is AccountViewModelParams.Prefilled -> { - flowOf(AccountScreenState.Success(params.accountInfo)) - } +) : AndroidViewModel(application) { + + private val _initialInfo = MutableStateFlow(null) + + private val _state = MutableStateFlow(AccountScreenState.Loading) + val state = _state.asStateFlow() + + val hasChanges = combine(_initialInfo, state) { initialInfo, state -> + state is AccountScreenState.Success && state.info != initialInfo }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), - initialValue = AccountScreenState.Loading + initialValue = false ) - fun saveData(data: DomainAccountInfo) { - viewModelScope.launch { - accounts.putAccount(data) + val canSave = state.map { + it is AccountScreenState.Success && it.info.isValid() + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = false + ) + + init { + when (params) { + is AccountViewModelParams.Id -> { + accounts.getAccountInfo(params.id) + .onEach { + _initialInfo.value = it + _state.value = AccountScreenState.Success(it) + }.catch { + _state.value = AccountScreenState.Error(it.localizedMessage ?: it.message ?: it.stackTraceToString()) + }.launchIn(viewModelScope) + } + is AccountViewModelParams.Prefilled -> { + _initialInfo.value = params.accountInfo + _state.value = AccountScreenState.Success(params.accountInfo) + } + } + } + + private inline fun mutateState(mutation: (DomainAccountInfo) -> DomainAccountInfo) { + _state.update { + if (it !is AccountScreenState.Success) return@update it + + AccountScreenState.Success(mutation(it.info)) + } + } + + fun updateIcon(uri: Uri?) { + if (uri == null) return + + val state = state.value + if (state !is AccountScreenState.Success) return + + viewModelScope.launch(Dispatchers.IO) { + val context = getApplication() + try { + val contentResolver = context.contentResolver + val bitmap = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + MediaStore.Images.Media.getBitmap(contentResolver, uri) + } else { + val source = ImageDecoder.createSource(contentResolver, uri) + ImageDecoder.decodeBitmap(source).copy(Bitmap.Config.ARGB_8888, false) + } + + val destination = File(context.filesDir, "${state.info.id}_${UUID.randomUUID()}.png").apply { + if (exists()) { + delete() + } + createNewFile() + } + destination.outputStream().use { out -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) + } + state.info.icon?.toFile()?.delete() + mutateState { it.copy(icon = destination.toUri()) } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun removeIcon() { + mutateState { + it.copy(icon = null) + } + } + + fun updateLabel(label: String) { + mutateState { + it.copy(label = label) + } + } + + fun updateIssuer(issuer: String) { + mutateState { + it.copy(issuer = issuer) + } + } + + fun updateSecret(secret: String) { + mutateState { + it.copy(secret = secret) + } + } + + fun updateType(otpType: OtpType) { + mutateState { + it.copy(type = otpType) + } + } + + fun updateDigest(otpDigest: OtpDigest) { + mutateState { + it.copy(algorithm = otpDigest) + } + } + + fun updateDigits(digits: String) { + mutateState { + it.copy(digits = digits) + } + } + + fun updateCounter(counter: String) { + mutateState { + it.copy(counter = counter) + } + } + + fun updatePeriod(period: String) { + mutateState { + it.copy(period = period) + } + } + + fun saveData() { + val state = state.value + if (state is AccountScreenState.Success) { + viewModelScope.launch { + accounts.putAccount(state.info) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenSuccess.kt b/app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenSuccess.kt index fe1a2a5..7220126 100644 --- a/app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenSuccess.kt +++ b/app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenSuccess.kt @@ -8,7 +8,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.with +import androidx.compose.animation.togetherWith import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -44,9 +44,9 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import com.xinto.mauth.R -import com.xinto.mauth.core.contracts.PickVisualMediaPersistent import com.xinto.mauth.core.otp.model.OtpDigest import com.xinto.mauth.core.otp.model.OtpType +import com.xinto.mauth.domain.account.model.DomainAccountInfo import com.xinto.mauth.ui.component.UriImage import com.xinto.mauth.ui.screen.account.component.AccountComboBox import com.xinto.mauth.ui.screen.account.component.AccountDataField @@ -55,31 +55,21 @@ import java.util.UUID @Composable fun AccountScreenSuccess( - id: UUID?, - icon: Uri?, + info: DomainAccountInfo, onIconChange: (Uri?) -> Unit, - label: String, onLabelChange: (String) -> Unit, - issuer: String, onIssuerChange: (String) -> Unit, - secret: String, onSecretChange: (String) -> Unit, - type: OtpType, onTypeChange: (OtpType) -> Unit, - digest: OtpDigest, onDigestChange: (OtpDigest) -> Unit, - digits: String, onDigitsChange: (String) -> Unit, - counter: String, onCounterChange: (String) -> Unit, - period: String, onPeriodChange: (String) -> Unit, ) { val imageSelectLauncher = rememberLauncherForActivityResult( - PickVisualMediaPersistent() - ) { - onIconChange(it) - } + contract = ActivityResultContracts.PickVisualMedia(), + onResult = onIconChange + ) LazyVerticalGrid( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -87,7 +77,7 @@ fun AccountScreenSuccess( contentPadding = PaddingValues(16.dp), columns = GridCells.Fixed(2) ) { - singleItem { + singleItem(key = "icon") { Box( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center @@ -108,8 +98,8 @@ fun AccountScreenSuccess( ) } ) { - if (icon != null) { - UriImage(uri = icon) + if (info.icon != null) { + UriImage(uri = info.icon) } else { Box(contentAlignment = Alignment.Center) { Icon( @@ -122,9 +112,9 @@ fun AccountScreenSuccess( } } } - singleItem { + singleItem(key = "label") { AccountDataField( - value = label, + value = info.label, onValueChange = onLabelChange, label = { Text(stringResource(R.string.account_data_label)) @@ -138,9 +128,9 @@ fun AccountScreenSuccess( required = true ) } - singleItem { + singleItem(key = "issuer") { AccountDataField( - value = issuer, + value = info.issuer, onValueChange = onIssuerChange, label = { Text(stringResource(R.string.account_data_issuer)) @@ -153,10 +143,10 @@ fun AccountScreenSuccess( }, ) } - singleItem { + singleItem(key = "secret") { var secretShown by remember { mutableStateOf(false) } AccountDataField( - value = secret, + value = info.secret, onValueChange = onSecretChange, label = { Text(stringResource(R.string.account_data_secret)) @@ -188,29 +178,29 @@ fun AccountScreenSuccess( required = true ) } - item { + item(key = "type") { AccountComboBox( values = OtpType.entries, - value = type, + value = info.type, onValueChange = onTypeChange, label = { Text(stringResource(R.string.account_data_type)) } ) } - item { + item(key = "digest") { AccountComboBox( values = OtpDigest.entries, - value = digest, + value = info.algorithm, onValueChange = onDigestChange, label = { Text(stringResource(R.string.account_data_algorithm)) } ) } - item { + item(key = "digits") { AccountNumberField( - value = digits, + value = info.digits, onValueChange = onDigitsChange, label = { Text(stringResource(R.string.account_data_digits)) @@ -222,11 +212,11 @@ fun AccountScreenSuccess( } ) } - item { + item(key = "period/counter") { AnimatedContent( - targetState = type, + targetState = info.type, transitionSpec = { - slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) + fadeIn() with + slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) + fadeIn() togetherWith slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Up) + fadeOut() }, label = "HOTP/TOTP" @@ -234,7 +224,7 @@ fun AccountScreenSuccess( when (it) { OtpType.TOTP -> { AccountNumberField( - value = period, + value = info.period, onValueChange = onPeriodChange, label = { Text(stringResource(R.string.account_data_period)) @@ -245,7 +235,7 @@ fun AccountScreenSuccess( } OtpType.HOTP -> { AccountNumberField( - value = counter, + value = info.counter, onValueChange = onCounterChange, label = { Text(stringResource(R.string.account_data_counter)) @@ -255,15 +245,13 @@ fun AccountScreenSuccess( } } } - if (id != null) { - singleItem { - Text( - modifier = Modifier.padding(top = 4.dp), - text = id.toString(), - style = MaterialTheme.typography.labelLarge, - color = LocalContentColor.current.copy(alpha = 0.7f) - ) - } + singleItem(key = "id") { + Text( + modifier = Modifier.padding(top = 4.dp), + text = info.id.toString(), + style = MaterialTheme.typography.labelLarge, + color = LocalContentColor.current.copy(alpha = 0.7f) + ) } } } From e3bdfba2b5632bfdf52dd6d7b8eea0d253eebe31 Mon Sep 17 00:00:00 2001 From: Xinto Date: Sun, 28 Apr 2024 12:14:08 +0400 Subject: [PATCH 4/7] Enable compose compiler reports and pass stability configuration to it --- app/build.gradle.kts | 19 +++++++++++++++++++ app/compose_stability.conf | 1 + 2 files changed, 20 insertions(+) create mode 100644 app/compose_stability.conf diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 24caa17..705d98b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,6 +47,25 @@ android { "-opt-in=androidx.compose.animation.ExperimentalAnimationApi" + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" + "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi" + + freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" + + "${projectDir.absolutePath}/compose_stability.conf" + ) + + if (project.findProperty("composeCompilerReports") == "true") { + freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.absolutePath}/compose_compiler" + ) + } + if (project.findProperty("composeCompilerMetrics") == "true") { + freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.buildDir.absolutePath}/compose_compiler" + ) + } } buildFeatures { diff --git a/app/compose_stability.conf b/app/compose_stability.conf new file mode 100644 index 0000000..736bd20 --- /dev/null +++ b/app/compose_stability.conf @@ -0,0 +1 @@ +kotlin.enums.EnumEntries \ No newline at end of file From 69d7bb6f84e394169817f7f04ac8c1618c4c6e3f Mon Sep 17 00:00:00 2001 From: Xinto Date: Sun, 28 Apr 2024 13:22:58 +0400 Subject: [PATCH 5/7] Update dependencies --- app/build.gradle.kts | 17 +++++++++-------- .../ui/component/ResponsiveAppBarScaffold.kt | 15 --------------- .../com/xinto/mauth/ui/component/TwoPaneCard.kt | 4 ++-- build.gradle.kts | 6 +++--- 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 705d98b..8516c67 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,16 +54,17 @@ android { "${projectDir.absolutePath}/compose_stability.conf" ) + val buildDir = layout.buildDirectory.asFile.get().absolutePath if (project.findProperty("composeCompilerReports") == "true") { freeCompilerArgs += listOf( "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.absolutePath}/compose_compiler" + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${buildDir}/compose_compiler" ) } if (project.findProperty("composeCompilerMetrics") == "true") { freeCompilerArgs += listOf( "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.buildDir.absolutePath}/compose_compiler" + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${buildDir}/compose_compiler" ) } } @@ -74,7 +75,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = "1.5.10" + kotlinCompilerExtensionVersion = "1.5.12" } packaging { @@ -102,13 +103,13 @@ ksp { } dependencies { - implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.core:core-ktx:1.13.0") implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") - implementation("androidx.activity:activity-compose:1.8.2") + implementation("androidx.activity:activity-compose:1.9.0") - val composeBom = platform("androidx.compose:compose-bom:2024.03.00") + val composeBom = platform("androidx.compose:compose-bom:2024.04.01") implementation(composeBom) implementation("androidx.compose.foundation:foundation") implementation("androidx.compose.material:material-icons-extended") @@ -120,7 +121,7 @@ dependencies { debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") - val cameraxVersion = "1.3.2" + val cameraxVersion = "1.3.3" implementation("androidx.camera:camera-core:$cameraxVersion") implementation("androidx.camera:camera-camera2:$cameraxVersion") implementation("androidx.camera:camera-view:$cameraxVersion") @@ -134,7 +135,7 @@ dependencies { implementation("androidx.biometric:biometric:1.1.0") implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06") - implementation("androidx.datastore:datastore-preferences:1.0.0") + implementation("androidx.datastore:datastore-preferences:1.1.0") implementation("dev.olshevski.navigation:reimagined:1.5.0") diff --git a/app/src/main/java/com/xinto/mauth/ui/component/ResponsiveAppBarScaffold.kt b/app/src/main/java/com/xinto/mauth/ui/component/ResponsiveAppBarScaffold.kt index 53ef7cc..91043ee 100644 --- a/app/src/main/java/com/xinto/mauth/ui/component/ResponsiveAppBarScaffold.kt +++ b/app/src/main/java/com/xinto/mauth/ui/component/ResponsiveAppBarScaffold.kt @@ -3,7 +3,6 @@ package com.xinto.mauth.ui.component import android.app.Activity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.material3.BottomAppBar import androidx.compose.material3.CenterAlignedTopAppBar @@ -15,15 +14,10 @@ import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSiz import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp /** * @param actions Aligned respective to the top appbar (3-dot menu last) @@ -61,15 +55,6 @@ fun ResponsiveAppBarScaffold( }, bottomBar = { if (sizeClass.widthSizeClass != WindowWidthSizeClass.Expanded) { - val currentDirection = LocalLayoutDirection.current - val newDirection by remember { - derivedStateOf { - when (currentDirection) { - LayoutDirection.Ltr -> LayoutDirection.Rtl - LayoutDirection.Rtl -> LayoutDirection.Ltr - } - } - } BottomAppBar( actions = { actions(Arrangement.Reverse) diff --git a/app/src/main/java/com/xinto/mauth/ui/component/TwoPaneCard.kt b/app/src/main/java/com/xinto/mauth/ui/component/TwoPaneCard.kt index 06d2e01..1baa829 100644 --- a/app/src/main/java/com/xinto/mauth/ui/component/TwoPaneCard.kt +++ b/app/src/main/java/com/xinto/mauth/ui/component/TwoPaneCard.kt @@ -4,8 +4,8 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -42,7 +42,7 @@ fun TwoPaneCard( visible = expanded, ) { Column { - Divider(Modifier.padding(vertical = 12.dp)) + HorizontalDivider(Modifier.padding(vertical = 12.dp)) bottomContent() } } diff --git a/build.gradle.kts b/build.gradle.kts index 071c076..3ff3b4d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.android.application") version "8.3.0" apply false - kotlin("android") version "1.9.22" apply false - id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false + id("com.android.application") version "8.3.2" apply false + kotlin("android") version "1.9.23" apply false + id("com.google.devtools.ksp") version "1.9.23-1.0.20" apply false } \ No newline at end of file From 5f7667605043708ccfa8a401d4a8c0931406f907 Mon Sep 17 00:00:00 2001 From: Xinto Date: Fri, 16 Aug 2024 00:40:01 +0400 Subject: [PATCH 6/7] Update the responsive scaffold menu item arrangement behavior --- .../ui/component/ResponsiveAppBarScaffold.kt | 4 +- .../ui/screen/home/component/HomeScaffold.kt | 93 ++++++++++--------- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/xinto/mauth/ui/component/ResponsiveAppBarScaffold.kt b/app/src/main/java/com/xinto/mauth/ui/component/ResponsiveAppBarScaffold.kt index 91043ee..007e60d 100644 --- a/app/src/main/java/com/xinto/mauth/ui/component/ResponsiveAppBarScaffold.kt +++ b/app/src/main/java/com/xinto/mauth/ui/component/ResponsiveAppBarScaffold.kt @@ -42,7 +42,7 @@ fun ResponsiveAppBarScaffold( TopAppBar( title = appBarTitle, actions = { - actions(Arrangement.Start) + actions(Arrangement.Reverse) }, scrollBehavior = scrollBehavior ) @@ -57,7 +57,7 @@ fun ResponsiveAppBarScaffold( if (sizeClass.widthSizeClass != WindowWidthSizeClass.Expanded) { BottomAppBar( actions = { - actions(Arrangement.Reverse) + actions(Arrangement.Start) }, floatingActionButton = floatingActionButton ) diff --git a/app/src/main/java/com/xinto/mauth/ui/screen/home/component/HomeScaffold.kt b/app/src/main/java/com/xinto/mauth/ui/screen/home/component/HomeScaffold.kt index 85e8658..d1da63f 100644 --- a/app/src/main/java/com/xinto/mauth/ui/screen/home/component/HomeScaffold.kt +++ b/app/src/main/java/com/xinto/mauth/ui/screen/home/component/HomeScaffold.kt @@ -69,52 +69,6 @@ fun HomeScaffold( horizontalArrangement = arrangement, verticalAlignment = Alignment.CenterVertically, ) { - var isSortVisible by remember { mutableStateOf(false) } - IconButton(onClick = { - isSortVisible = true - }) { - Icon( - painter = painterResource(R.drawable.ic_sort), - contentDescription = null - ) - DropdownMenu( - expanded = isSortVisible, - onDismissRequest = { - isSortVisible = false - } - ) { - SortSetting.entries.forEach { - DropdownMenuItem( - onClick = { - isSortVisible = false - onActiveSortChange(it) - }, - text = { - val resource = remember(it) { - when (it) { - SortSetting.DateAsc -> R.string.home_sort_date_ascending - SortSetting.DateDesc -> R.string.home_sort_date_descending - SortSetting.LabelAsc -> R.string.home_sort_label_ascending - SortSetting.LabelDesc -> R.string.home_sort_label_descending - SortSetting.IssuerAsc -> R.string.home_sort_issuer_ascending - SortSetting.IssuerDesc -> R.string.home_sort_issuer_descending - } - } - Text(stringResource(resource)) - }, - trailingIcon = { - if (activeSortSetting == it) { - Icon( - painter = painterResource(R.drawable.ic_check), - contentDescription = null - ) - } - } - ) - } - } - } - var isMoreActionsVisible by remember { mutableStateOf(false) } IconButton(onClick = { isMoreActionsVisible = true @@ -161,6 +115,53 @@ fun HomeScaffold( ) } } + + var isSortVisible by remember { mutableStateOf(false) } + IconButton(onClick = { + isSortVisible = true + }) { + Icon( + painter = painterResource(R.drawable.ic_sort), + contentDescription = null + ) + DropdownMenu( + expanded = isSortVisible, + onDismissRequest = { + isSortVisible = false + } + ) { + SortSetting.entries.forEach { + DropdownMenuItem( + onClick = { + isSortVisible = false + onActiveSortChange(it) + }, + text = { + val resource = remember(it) { + when (it) { + SortSetting.DateAsc -> R.string.home_sort_date_ascending + SortSetting.DateDesc -> R.string.home_sort_date_descending + SortSetting.LabelAsc -> R.string.home_sort_label_ascending + SortSetting.LabelDesc -> R.string.home_sort_label_descending + SortSetting.IssuerAsc -> R.string.home_sort_issuer_ascending + SortSetting.IssuerDesc -> R.string.home_sort_issuer_descending + } + } + Text(stringResource(resource)) + }, + trailingIcon = { + if (activeSortSetting == it) { + Icon( + painter = painterResource(R.drawable.ic_check), + contentDescription = null + ) + } + } + ) + } + } + } + } } } From fc524c058277ccade678484392e8c96f2b6fbc0c Mon Sep 17 00:00:00 2001 From: Xinto Date: Thu, 29 Aug 2024 00:11:38 +0400 Subject: [PATCH 7/7] Fix auth logic --- .../com/xinto/mauth/ui/screen/auth/AuthScreen.kt | 6 +----- .../xinto/mauth/ui/screen/auth/AuthViewModel.kt | 14 ++++---------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthScreen.kt b/app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthScreen.kt index 321df1e..b9a32ed 100644 --- a/app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthScreen.kt @@ -56,11 +56,7 @@ fun AuthScreen( AuthScreen( modifier = modifier, code = code, - onNumberAdd = { - if (viewModel.insertNumber(it)) { - onAuthSuccess() - } - }, + onNumberAdd = viewModel::insertNumber, onNumberDelete = viewModel::deleteNumber, onClear = viewModel::clear, showFingerprint = canUseBiometrics, diff --git a/app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthViewModel.kt index af3ddb2..bd07cf6 100644 --- a/app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthViewModel.kt @@ -7,10 +7,8 @@ import com.xinto.mauth.domain.SettingsRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.runBlocking class AuthViewModel( private val authRepository: AuthRepository, @@ -27,10 +25,8 @@ class AuthViewModel( initialValue = false ) - fun insertNumber(number: Char): Boolean { - return _code.getAndUpdate { - it + number - } == "5746" + fun insertNumber(number: Char) { + _code.update { it + number } } fun deleteNumber() { @@ -41,9 +37,7 @@ class AuthViewModel( _code.value = "" } - fun validate(code: String): Boolean { - return runBlocking { - authRepository.validate(code) - } + suspend fun validate(code: String): Boolean { + return authRepository.validate(code) } } \ No newline at end of file