diff --git a/Maestro/.gitignore b/Maestro/.gitignore new file mode 100644 index 0000000..ca7dc48 --- /dev/null +++ b/Maestro/.gitignore @@ -0,0 +1 @@ +.maestro \ No newline at end of file diff --git a/Maestro/flow.yaml b/Maestro/flow.yaml new file mode 100644 index 0000000..a96d651 --- /dev/null +++ b/Maestro/flow.yaml @@ -0,0 +1,37 @@ +appId: dev.mi6e4ka.openstore +--- +- launchApp: + clearState: true + +- assertVisible: "OpenStore" +- takeScreenshot: "01_main_screen" + +- tapOn: "Search" +- inputText: "Mir Pay" +- pressKey: Enter +- assertVisible: "Mir Pay" +- takeScreenshot: "02_search_screen" + +- tapOn: "Mir Pay" +- assertVisible: "Mir Pay" +- takeScreenshot: "03_details_screen" + +- scrollUntilVisible: + element: "Rating and reviews" + direction: DOWN +- tapOn: "Rating and reviews" +- assertVisible: "Reviews" +- takeScreenshot: "04_review_screen" + +- back +- back +- back +- assertVisible: "OpenStore" +- tapOn: "updates|обновления" +- assertVisible: "Updates" +- takeScreenshot: "05_updates_screen" + +- back +- tapOn: "settings|настройки" +- assertVisible: "Settings|Настройки" +- takeScreenshot: "06_settings_screen" \ No newline at end of file diff --git a/README.md b/README.md index 9c8f532..04104d7 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,12 @@ Verification info: - [ ] ...maybe more? ## Screenshots -[](https://codeberg.org/mi6e4ka/openstore/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png) -[](https://codeberg.org/mi6e4ka/openstore/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png) -[](https://codeberg.org/mi6e4ka/openstore/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png) -[](https://codeberg.org/mi6e4ka/openstore/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png) -[](https://codeberg.org/mi6e4ka/openstore/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png) + + + + + + ## Building the app See [here](https://codeberg.org/mi6e4ka/openstore/src/branch/main/BUILDING.md) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 90144cc..7deb6c6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,6 @@ android:supportsRtl="true" android:theme="@style/AppTheme" android:localeConfig="@xml/locale_config" - android:networkSecurityConfig="@xml/network_security_config" tools:targetApi="36" android:memtagMode="async"> - - - - - + + + + + + @@ -76,4 +76,4 @@ android:name=".internal.installer.PackageInstallerStatusReceiver" android:exported="false" /> - \ No newline at end of file + diff --git a/app/src/main/java/dev/mi6e4ka/openstore/MainActivity.kt b/app/src/main/java/dev/mi6e4ka/openstore/MainActivity.kt index 84a8635..5df0999 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/MainActivity.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/MainActivity.kt @@ -3,11 +3,13 @@ package dev.mi6e4ka.openstore import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.layout.fillMaxSize @@ -34,6 +36,7 @@ import dev.mi6e4ka.openstore.ui.theme.AppTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) val cacheDir = externalCacheDir ?: cacheDir cacheDir.listFiles()?.forEach { file -> @@ -66,20 +69,34 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { val navController = rememberNavController() + val duration = 300 + val easing = FastOutSlowInEasing NavHost( navController = navController, startDestination = "search", enterTransition = { - slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(300)) + slideInHorizontally( + initialOffsetX = { (it * 0.1f).toInt() }, + animationSpec = tween(duration, easing = easing) + ) + fadeIn(animationSpec = tween(duration)) }, exitTransition = { - slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(300)) + slideOutHorizontally( + targetOffsetX = { -(it * 0.1f).toInt() }, + animationSpec = tween(duration, easing = easing) + ) + fadeOut(animationSpec = tween(duration)) }, popEnterTransition = { - slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(300)) + slideInHorizontally( + initialOffsetX = { -(it * 0.1f).toInt() }, + animationSpec = tween(duration, easing = easing) + ) + fadeIn(animationSpec = tween(duration)) }, popExitTransition = { - slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(300)) + slideOutHorizontally( + targetOffsetX = { (it * 0.1f).toInt() }, + animationSpec = tween(duration, easing = easing) + ) + fadeOut(animationSpec = tween(duration)) } ) { composable("search") { diff --git a/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/ApkInstaller.kt b/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/ApkInstaller.kt index 8471d34..bedb935 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/ApkInstaller.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/ApkInstaller.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -63,6 +64,8 @@ class ApkInstaller(private val context: Context) { val totalBytesToDownload = files.sumOf { it.size } val totalBytesDownloaded = AtomicLong(0) val downloadedFiles = Collections.synchronizedList(mutableListOf()) + var lastUpdateMillis = 0L + try { val resultingFiles = coroutineScope { files.map { file -> @@ -70,11 +73,12 @@ class ApkInstaller(private val context: Context) { val fileName = file.url.split("/").last() val apkFile = getApkFile(fileName) if (!downloadedFiles.contains(apkFile)) { - downloadedPackages.add(apkFile) + downloadedFiles.add(apkFile) } if (apkFile.exists() && apkFile.length() == file.size) { if (verifyFileHash(apkFile, file.hash)) { + totalBytesDownloaded.addAndFetch(file.size) return@async apkFile } apkFile.delete() @@ -83,42 +87,38 @@ class ApkInstaller(private val context: Context) { val request = Request.Builder().url(file.url).build() val call = okHttpClient.newCall(request) - // Связываем отмену корутины с отменой сетевого вызова - val job = coroutineContext[Job] - val cancellationDisposable = job?.invokeOnCompletion { - if (job.isCancelled) call.cancel() - } + // Связываем отмену корутины с OkHttp + val job = coroutineContext[Job]!! + val cancelHandler = job.invokeOnCompletion { call.cancel() } try { call.execute().use { response -> if (!response.isSuccessful) throw IOException("Ошибка загрузки: ${response.code}") - val responseBody = - response.body ?: throw IOException("Пустой ответ от сервера") + val responseBody = response.body ?: throw IOException("Пустой ответ") responseBody.byteStream().use { inputStream -> FileOutputStream(apkFile).use { outputStream -> - val buffer = ByteArray(8192) + val buffer = ByteArray(16384) // Увеличил буфер для скорости var bytesRead: Int while (inputStream.read(buffer).also { bytesRead = it } != -1) { - // Проверяем статус отмены на каждой итерации - if (call.isCanceled()) { - throw IOException("Canceled") - } - + job.ensureActive() outputStream.write(buffer, 0, bytesRead) - val newTotalDownloaded = - totalBytesDownloaded.addAndFetch(bytesRead.toLong()) - val percent = if (totalBytesToDownload > 0) { - (newTotalDownloaded * 100 / totalBytesToDownload).toInt() - } else { - 0 - } - - withContext(Dispatchers.Main) { - onProgress(percent, newTotalDownloaded, totalBytesToDownload) + val currentTotal = totalBytesDownloaded.addAndFetch(bytesRead.toLong()) + + // Троттлинг обновлений UI (не чаще 10 раз в секунду) + val now = System.currentTimeMillis() + if (now - lastUpdateMillis > 100) { + lastUpdateMillis = now + val percent = if (totalBytesToDownload > 0) { + (currentTotal * 100 / totalBytesToDownload).toInt() + } else 0 + + withContext(Dispatchers.Main) { + onProgress(percent, currentTotal, totalBytesToDownload) + } } } outputStream.flush() @@ -128,11 +128,10 @@ class ApkInstaller(private val context: Context) { if (!verifyFileHash(apkFile, file.hash)) { throw HashMismatchException(apkFile, file.hash) } - apkFile } } finally { - cancellationDisposable?.dispose() + cancelHandler.dispose() } } }.awaitAll() @@ -142,7 +141,6 @@ class ApkInstaller(private val context: Context) { onSuccess(resultingFiles) } } catch (e: Exception) { - // Выполняем очистку и уведомление в NonCancellable, чтобы код не пролетел мимо withContext(NonCancellable) { if (e is HashMismatchException) { val expectedHash = formatHashForDisplay(e.expectedHash) @@ -152,14 +150,11 @@ class ApkInstaller(private val context: Context) { onInvalidHash(filesCopy, expectedHash, actualHash) } } else { - downloadedFiles.forEach { file -> - if (file.exists()) { - file.delete() - } - } + // Удаляем недокачанные файлы при любой другой ошибке или отмене + downloadedFiles.forEach { if (it.exists()) it.delete() } withContext(Dispatchers.Main) { - if (e is CancellationException || (e is IOException && e.message == "Canceled")) { + if (e is CancellationException || (e is IOException && callIsCanceled(e))) { onCancel() } else { onError(e) @@ -170,13 +165,13 @@ class ApkInstaller(private val context: Context) { } } - return { - downloadRunner.cancel() - } + return { downloadRunner.cancel() } + } + + private fun callIsCanceled(e: IOException): Boolean { + return e.message?.contains("Canceled", ignoreCase = true) == true || + e.message?.contains("Socket closed", ignoreCase = true) == true } - - // Вспомогательный список для очистки (дублирует логику для надежности) - private val downloadedPackages = Collections.synchronizedList(mutableListOf()) private class HashMismatchException(val file: File, val expectedHash: String) : IOException("Hash mismatch") @@ -199,8 +194,6 @@ class ApkInstaller(private val context: Context) { private fun verifyFileHash(file: File, expectedHash: String): Boolean { val normalizedHash = expectedHash.trim().ifEmpty { return true } val expectedBytes = decodeHashValue(normalizedHash) ?: return true - - // Only XXH64 is supported now (8 bytes) if (expectedBytes.size != 8) return true val actualBytes = digestFileXXH64(file) ?: return false diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/components/Gallery.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/components/Gallery.kt index 0865a21..4fc242d 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/components/Gallery.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/components/Gallery.kt @@ -33,6 +33,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -62,7 +65,8 @@ fun GalleryWithPager(images: List) { modifier = Modifier.height(250.dp) ) { LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.clearAndSetSemantics{} ) { itemsIndexed(images) { i, file -> var isLoading by remember { mutableStateOf(true) } diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/details/DetailsScreen.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/details/DetailsScreen.kt index 8212a32..1601359 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/details/DetailsScreen.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/details/DetailsScreen.kt @@ -86,6 +86,9 @@ import kotlinx.datetime.toLocalDateTime import android.Manifest import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import kotlin.time.ExperimentalTime import kotlin.time.Instant @@ -141,7 +144,7 @@ fun DetailsScreen( IconButton(onClick = { navController.popBackStack() }) { Icon( painter = painterResource(R.drawable.ic_arrow_back_24px), - contentDescription = "Back" + contentDescription = stringResource(R.string.back_button_desc) ) } }, @@ -153,7 +156,10 @@ fun DetailsScreen( } } ){ - Icon(painterResource(R.drawable.ic_link_24px), null) + Icon( + painterResource(R.drawable.ic_link_24px), + stringResource(R.string.copy_link_desc) + ) } IconButton( { @@ -175,7 +181,10 @@ fun DetailsScreen( } } ){ - Icon(painterResource(R.drawable.apk_install_24px), null) + Icon( + painterResource(R.drawable.apk_install_24px), + stringResource(R.string.download_apk_desc) + ) } IconButton(onClick = { val intent = Intent(Intent.ACTION_VIEW, "https://www.rustore.ru/catalog/app/${state.item?.packageName ?: ""}".toUri()).apply { @@ -184,7 +193,10 @@ fun DetailsScreen( } context.startActivity(intent) }, enabled = state.item != null) { - Icon(painterResource(R.drawable.ic_captive_portal_24px), null) + Icon( + painterResource(R.drawable.ic_captive_portal_24px), + stringResource(R.string.open_in_browser_desc) + ) } } ) } @@ -332,7 +344,7 @@ fun DetailsScreen( verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text(item.appName, fontSize = 24.sp, fontWeight = FontWeight.Bold, maxLines = 2, overflow = TextOverflow.Ellipsis) - Text(item.aggregatorInfo?.companyName ?: item.companyName, maxLines = 2, overflow = TextOverflow.Ellipsis) + Text(item.aggregatorInfo?.companyName ?: item.companyName, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.clearAndSetSemantics(){contentDescription = "Разработчик: ${item.aggregatorInfo?.companyName ?: item.companyName}"}) } } if (!state.isSupported) { @@ -433,18 +445,30 @@ fun DetailsScreen( (state.appFiles?.sumOf { it.size } ?: 0L) / 1024.0 / 1024.0) ) }, - leadingIcon = {Icon(painterResource(R.drawable.ic_upload_file_24px), contentDescription = null)}, + leadingIcon = { + Icon( + painterResource(R.drawable.ic_upload_file_24px), + stringResource(R.string.app_size_desc) + )}, onClick = {}, ) AssistChip( onClick = {}, label = {Text(item.versionName)}, - leadingIcon = {Icon(painterResource(R.drawable.ic_arrow_circle_up_24px), contentDescription = null)} + leadingIcon = { + Icon( + painterResource(R.drawable.ic_arrow_circle_up_24px), + stringResource(R.string.latest_version_desc) + )} ) AssistChip( onClick = {}, label = {FormattedDateText(item.appVerUpdatedAt)}, - leadingIcon = {Icon(painterResource(R.drawable.ic_update_24px), contentDescription = null)} + leadingIcon = { + Icon( + painterResource(R.drawable.ic_update_24px), + stringResource(R.string.last_update_desc) + )} ) var detailedApiVersion by remember { mutableStateOf(false) } AssistChip( @@ -458,22 +482,38 @@ fun DetailsScreen( Text("API ${item.minSdkVersion} +") } }, - leadingIcon = {Icon(painterResource(R.drawable.ic_android_24px), contentDescription = null)} + leadingIcon = { + Icon( + painterResource(R.drawable.ic_android_24px), + stringResource(R.string.min_api_desc) + )} ) AssistChip( onClick = {}, label = {Text(if ((state.appFiles?.size ?: 1) > 1) "${state.appFiles?.size} Split APKs" else "APK")}, - leadingIcon = {Icon(painterResource(R.drawable.ic_adb_24px), contentDescription = null)} + leadingIcon = { + Icon( + painterResource(R.drawable.ic_adb_24px), + stringResource(R.string.installer_type_desc) + )} ) AssistChip( onClick = {}, label = {Text(item.roundedDownloadsText)}, - leadingIcon = {Icon(painterResource(R.drawable.ic_download_24px), contentDescription = null)} + leadingIcon = { + Icon( + painterResource(R.drawable.ic_download_24px), + stringResource(R.string.downloads_desc) + )} ) AssistChip( onClick = {}, label = {Text(item.ageLegal)}, - leadingIcon = {Icon(painterResource(R.drawable.ic_supervised_user_circle_24px), contentDescription = null)} + leadingIcon = { + Icon( + painterResource(R.drawable.ic_supervised_user_circle_24px), + stringResource(R.string.age_legal_desc) + )} ) } GalleryWithPager(item.fileUrls) @@ -539,6 +579,13 @@ fun DetailsScreen( restoreState = true } }) + .clearAndSetSemantics { + contentDescription = context.getString( + R.string.reviews_desc, + state.rating?.averageUserRating ?: 0, + state.rating?.totalRatings ?: 0 + ) + } ) { Row( Modifier diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/results/ResultsScreen.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/results/ResultsScreen.kt index a84fd15..811dd57 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/results/ResultsScreen.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/results/ResultsScreen.kt @@ -35,6 +35,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -68,22 +70,25 @@ fun ResultsScreen( IconButton(onClick = { navController.popBackStack() }) { Icon( painterResource(R.drawable.ic_arrow_back_24px), - contentDescription = "Back" + contentDescription = stringResource(R.string.back_button_desc) ) } }, actions = { val platforms = remember { listOf( - Platform("Mobile", "mobile", R.drawable.ic_mobile_24px), - Platform("TV", "tv", R.drawable.ic_tv_gen_24px) + Platform(R.string.mobile_label, "mobile", R.drawable.ic_mobile_24px), + Platform(R.string.tv_label, "tv", R.drawable.ic_tv_gen_24px) ) } TextButton({platformSelectorExpanded = true}) { - Icon(painterResource(state.selectedPlatform.icon), null) + Icon( + painterResource(state.selectedPlatform.icon), + contentDescription = stringResource(R.string.tts_current_platform) + ) Spacer(Modifier.size(8.dp)) - Text(state.selectedPlatform.label) + Text(stringResource(state.selectedPlatform.label)) } DropdownMenu( expanded = platformSelectorExpanded, @@ -92,7 +97,7 @@ fun ResultsScreen( platforms.forEach { DropdownMenuItem( leadingIcon = {Icon(painterResource(it.icon), null)}, - text = { Text(it.label) }, + text = { Text(stringResource(it.label)) }, onClick = { viewModel.selectPlatform(it); platformSelectorExpanded=false } ) } @@ -125,7 +130,16 @@ fun ResultsScreen( modifier = Modifier.weight(1f).padding(start = 16.dp,end = 4.dp) ) { Text(text = app.appName, maxLines = 1, overflow = TextOverflow.Ellipsis) - Text(text = app.packageName, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.secondary) + Text( + text = app.packageName, + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.semantics{ + hideFromAccessibility() + } + ) } } } diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/results/ResultsViewModel.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/results/ResultsViewModel.kt index 95866d2..5bfbf0b 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/results/ResultsViewModel.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/results/ResultsViewModel.kt @@ -17,12 +17,12 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.update data class Platform( - val label: String, + val label: Int, val value: String, val icon: Int ) data class ResultsState( - val selectedPlatform: Platform = Platform("Mobile", "mobile", R.drawable.ic_mobile_24px), + val selectedPlatform: Platform = Platform(R.string.mobile_label, "mobile", R.drawable.ic_mobile_24px), ) class ResultsViewModel(private val query: String) : ViewModel() { diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/reviews/ReviewsScreen.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/reviews/ReviewsScreen.kt index 2ac553e..56eea6e 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/reviews/ReviewsScreen.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/reviews/ReviewsScreen.kt @@ -35,8 +35,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -95,7 +99,7 @@ fun ReviewsScreen(packageName: String, navController: NavController = rememberNa IconButton(onClick = { navController.popBackStack() }) { Icon( painterResource(R.drawable.ic_arrow_back_24px), - contentDescription = "Back" + contentDescription = stringResource(R.string.back_button_desc) ) } }, @@ -135,11 +139,12 @@ fun ReviewsScreen(packageName: String, navController: NavController = rememberNa } items(reviews ?: listOf()) { review -> Card( - Modifier.fillMaxWidth() + Modifier.fillMaxWidth().semantics(mergeDescendants = true){} ) { Column( Modifier.padding(16.dp) ) { + val context = LocalContext.current Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -155,7 +160,10 @@ fun ReviewsScreen(packageName: String, navController: NavController = rememberNa } Spacer(Modifier.height(8.dp)) Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clearAndSetSemantics{ + contentDescription = context.getString(R.string.review_rating_desc, review.appRating) + } ) { repeat(review.appRating) { Icon(painterResource(R.drawable.ic_star_filled_24px), null, Modifier.size(16.dp), tint = MaterialTheme.colorScheme.primary) @@ -171,12 +179,12 @@ fun ReviewsScreen(packageName: String, navController: NavController = rememberNa horizontalArrangement = Arrangement.spacedBy(16.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { - Icon(painterResource(R.drawable.ic_thumb_up_24px), contentDescription = null, Modifier.size(20.dp), tint = MaterialTheme.colorScheme.secondary) + Icon(painterResource(R.drawable.ic_thumb_up_24px), contentDescription = stringResource(R.string.likes_desc), Modifier.size(20.dp), tint = MaterialTheme.colorScheme.secondary) Spacer(Modifier.width(4.dp)) Text("${review.likeCounter}") } Row(verticalAlignment = Alignment.CenterVertically) { - Icon(painterResource(R.drawable.ic_thumb_down_24px), contentDescription = null, Modifier.size(20.dp), tint = MaterialTheme.colorScheme.secondary) + Icon(painterResource(R.drawable.ic_thumb_down_24px), contentDescription = stringResource(R.string.dislikes_desc), Modifier.size(20.dp), tint = MaterialTheme.colorScheme.secondary) Spacer(Modifier.width(6.dp)) Text("${review.dislikeCounter}") } diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/search/SearchScreen.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/search/SearchScreen.kt index 4c35931..e5c319b 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/search/SearchScreen.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/search/SearchScreen.kt @@ -45,8 +45,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp @@ -95,7 +98,10 @@ fun SearchScreen(navController: NavController = rememberNavController()) { ) } IconButton({navController.navigate("settings")}) { - Icon(painterResource(R.drawable.ic_settings_24px), "settings") + Icon( + painterResource(R.drawable.ic_settings_24px), + contentDescription = stringResource(R.string.settings_title) + ) } } ) }, @@ -147,7 +153,9 @@ fun SearchScreen(navController: NavController = rememberNavController()) { value = search, onValueChange = {s -> search=s}, leadingIcon = { Icon(painterResource(R.drawable.ic_search_24px), contentDescription = null) }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().semantics{ + contentDescription = context.getString(R.string.search_title) + }, singleLine = true, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Search diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/settings/SettingsScreen.kt index e0bda11..a5cbd2f 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/settings/SettingsScreen.kt @@ -105,7 +105,7 @@ fun SettingsScreen( id = "updates_sources", title = R.string.settings_updates_sources_title, description = R.string.settings_updates_sources_description, - icon = R.drawable.ic_update_24px, + icon = R.drawable.ic_checklist_24px, type = ParamType.MULTI_SELECT, defaultVal = "dev.mi6e4ka.openstore;dev.mi6e4ka.openstore.nightly;dev.mi6e4ka.openstore.debug;ru.vk.store", select = dynamicSources @@ -114,7 +114,7 @@ fun SettingsScreen( id = "updates_check_all", title = R.string.settings_updates_check_all_title, description = R.string.settings_updates_check_all_description, - icon = R.drawable.ic_arrow_circle_up_24px, + icon = R.drawable.ic_arrow_shape_up_stack_2_24px, type = ParamType.SWITCH, defaultVal = "" ), @@ -127,15 +127,15 @@ fun SettingsScreen( defaultVal = "auto", select = listOf( SettingsSectionSelect( - title = "Auto", + title = context.getString(R.string.theme_auto_label), value = "auto" ), SettingsSectionSelect( - title = "Dark", + title = context.getString(R.string.theme_dark_label), value = "dark" ), SettingsSectionSelect( - title = "Light", + title = context.getString(R.string.theme_light_label), value = "light" ), ) @@ -156,7 +156,10 @@ fun SettingsScreen( } } ) { - Icon(painterResource(R.drawable.ic_arrow_back_24px), "back") + Icon( + painterResource(R.drawable.ic_arrow_back_24px), + stringResource(R.string.back_button_desc) + ) } } ) @@ -206,7 +209,12 @@ private fun getDynamicInstallerSources(context: Context): List + android:viewportWidth="130" + android:viewportHeight="130"> + android:pathData="M0,0h130v130h-130z"> - - - + + diff --git a/app/src/main/res/drawable/ic_arrow_shape_up_stack_2_24px.xml b/app/src/main/res/drawable/ic_arrow_shape_up_stack_2_24px.xml new file mode 100644 index 0000000..d973457 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_shape_up_stack_2_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_checklist_24px.xml b/app/src/main/res/drawable/ic_checklist_24px.xml new file mode 100644 index 0000000..28c2be7 --- /dev/null +++ b/app/src/main/res/drawable/ic_checklist_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 068a3c6..239cfed 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,14 +1,14 @@ - + android:viewportWidth="137" + android:viewportHeight="130"> + diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000..15794c1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 50ec886..04091bd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 50ec886..04091bd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 2af4a43..1424eb6 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index e883ea3..73244a6 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 706e6a4..9e24140 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index a053d4c..7bbd886 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index b8f9066..495ec0c 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index c4d13f3..46033b0 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index fa14656..f300bf0 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 8a53082..1e5938b 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 929a3a1..fc81277 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 54aa9de..9af7294 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 366395e..43e6f88 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -44,10 +44,10 @@ Settings Installer If there are problems installing applications - Source updates whitelist - Sources allowed to check for updates - Check all installed applications - Do not limit update checks to the whitelist. RuStore will get access to the entire list of installed applications. + Tracked sources + Updates will be checked for applications from selected sources + Check all applications + Do not limit the update check to selected sources. RuStore will get access to the entire list of applications on device Save Updates Check for updates @@ -68,4 +68,11 @@ Manual Installed Unknown (ADB/Other) Provided by developer + Mobile + TV + Auto + Dark + Light + Back + Apps for platfom \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 8bede2e..45af79c 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,6 +1,3 @@ - + +