mirror of
https://codeberg.org/mi6e4ka/openstore.git
synced 2026-07-09 15:59:50 +00:00
openstore pre release
This commit is contained in:
parent
29eaadf629
commit
5e66f44c1a
48 changed files with 2172 additions and 462 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -1,15 +1,12 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea
|
||||
/.kotlin
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
/app/release
|
||||
|
|
|
|||
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
|
|
@ -1,3 +0,0 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
6
.idea/AndroidProjectSystem.xml
generated
6
.idea/AndroidProjectSystem.xml
generated
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
18
.idea/deploymentTargetSelector.xml
generated
18
.idea/deploymentTargetSelector.xml
generated
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-06-30T15:13:37.033573869Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/mi6e4ka/.android/avd/Pixel_8a.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
19
.idea/gradle.xml
generated
19
.idea/gradle.xml
generated
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
61
.idea/inspectionProfiles/Project_Default.xml
generated
61
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -1,61 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
10
.idea/migrations.xml
generated
10
.idea/migrations.xml
generated
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/misc.xml
generated
9
.idea/misc.xml
generated
|
|
@ -1,9 +0,0 @@
|
|||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
17
.idea/runConfigurations.xml
generated
17
.idea/runConfigurations.xml
generated
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -6,26 +6,47 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "dev.mi6e4ka.openstore"
|
||||
compileSdk = 35
|
||||
compileSdk = 36
|
||||
|
||||
androidResources {
|
||||
localeFilters.addAll(arrayOf("ru", "en"))
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "dev.mi6e4ka.openstore"
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
targetSdk = 36
|
||||
versionCode = 6
|
||||
versionName = "2.0.0"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
create("nightly") {
|
||||
versionNameSuffix = "-nightly"
|
||||
applicationIdSuffix = ".nightly"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
resValue("string", "app_name", "OpenStore Nightly")
|
||||
}
|
||||
debug {
|
||||
versionNameSuffix = "-debug"
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "OpenStore Debug")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
|
@ -36,6 +57,7 @@ android {
|
|||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,8 +69,14 @@ dependencies {
|
|||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material.icons.extended.android)
|
||||
implementation(libs.accompanist.pager)
|
||||
implementation(libs.accompanist.pager.indicators)
|
||||
implementation(libs.androidx.runtime.livedata)
|
||||
implementation(libs.material3)
|
||||
debugImplementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.okhttp3.logging.interceptor)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
|
|
|||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
|
@ -18,4 +18,4 @@
|
|||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package dev.mi6e4ka.openstore
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("dev.mi6e4ka.openstore", appContext.packageName)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,15 @@
|
|||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"/>
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
|
|
@ -12,18 +21,38 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.OpenStore"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="36">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.OpenStore">
|
||||
<!-- openstore deeplink, haha -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:host="rustore.ru" />
|
||||
<data android:host="www.rustore.ru" />
|
||||
<data android:host="apps.rustore.ru" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<receiver
|
||||
android:name=".internal.installer.PackageInstallerStatusReceiver"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -3,173 +3,133 @@ package dev.mi6e4ka.openstore
|
|||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.EaseIn
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandIn
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.shrinkOut
|
||||
import androidx.compose.animation.slideIn
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavArgument
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import dev.mi6e4ka.openstore.ui.theme.OpenStoreTheme
|
||||
import androidx.navigation.navDeepLink
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import coil.request.CachePolicy
|
||||
import dev.mi6e4ka.openstore.ui.screen.details.DetailsScreen
|
||||
import dev.mi6e4ka.openstore.ui.screen.results.ResultsScreen
|
||||
import dev.mi6e4ka.openstore.ui.screen.reviews.ReviewsScreen
|
||||
import dev.mi6e4ka.openstore.ui.screen.search.SearchScreen
|
||||
import dev.mi6e4ka.openstore.ui.theme.Theme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val cacheDir = externalCacheDir ?: return
|
||||
cacheDir.listFiles()?.forEach { file ->
|
||||
if (file.isFile && file.name.endsWith(".apk", ignoreCase = true)) {
|
||||
file.delete()
|
||||
println("CacheCleaner" + "Удален: ${file.name}")
|
||||
}
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
val imageLoader = ImageLoader.Builder(this)
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(cacheDir.resolve("image_cache"))
|
||||
.maxSizeBytes(10L * 1024 * 1024)
|
||||
.build()
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder(this)
|
||||
.maxSizeBytes(10 * 1024 * 1024)
|
||||
.build()
|
||||
}
|
||||
.networkCachePolicy(CachePolicy.ENABLED)
|
||||
.diskCachePolicy(CachePolicy.ENABLED)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build()
|
||||
Coil.setImageLoader(imageLoader)
|
||||
|
||||
setContent {
|
||||
OpenStoreTheme {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController = navController, startDestination = "search") {
|
||||
composable("search") {
|
||||
MainPage(onSearch = {search ->
|
||||
navController.navigate("search/$search")
|
||||
})
|
||||
}
|
||||
composable("search/{query}",
|
||||
arguments = listOf(
|
||||
navArgument("query") {
|
||||
defaultValue = ""
|
||||
}
|
||||
)
|
||||
) { entry ->
|
||||
val query = entry.arguments?.getString("query")
|
||||
SearchScreen(navController=navController, query=query)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchScreen(navController: NavController, query: String?) {
|
||||
data class AppInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val iconUrl: String
|
||||
)
|
||||
|
||||
|
||||
Scaffold { innerPadding ->
|
||||
Column(modifier = Modifier.padding(innerPadding)) {
|
||||
TopAppBar(
|
||||
title = {Text("Поиск: $query")},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {navController.navigateUp()}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
Text("goida search $query")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainPage(onSearch: (String) -> Unit) {
|
||||
var search by remember { mutableStateOf("") }
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(bottom=10.dp, start=10.dp, end=10.dp, top=0.dp)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Column {
|
||||
TopAppBar(title = {Text(stringResource(R.string.app_name))})
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp, 0.dp)
|
||||
.height(50.dp)
|
||||
Theme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 10.dp)
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "search",
|
||||
enterTransition = {
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
250, easing = LinearEasing
|
||||
)
|
||||
)/* + slideIntoContainer(
|
||||
animationSpec = tween(300, easing = EaseIn),
|
||||
towards = AnimatedContentTransitionScope.SlideDirection.Start
|
||||
)*/
|
||||
},
|
||||
exitTransition = {
|
||||
fadeOut(
|
||||
animationSpec = tween(
|
||||
250, easing = LinearEasing
|
||||
)
|
||||
)/* + slideOutOfContainer(
|
||||
animationSpec = tween(300, easing = EaseOut),
|
||||
towards = AnimatedContentTransitionScope.SlideDirection.End
|
||||
)*/
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
)
|
||||
Text(stringResource(R.string.safety_card_text), fontSize = 18.sp)
|
||||
composable("search") {
|
||||
SearchScreen(navController)
|
||||
}
|
||||
composable("search/{query}") {
|
||||
val query = it.arguments?.getString("query") ?: ""
|
||||
ResultsScreen(query = query, navController = navController)
|
||||
}
|
||||
composable("app/{appId}", deepLinks = listOf(
|
||||
navDeepLink { uriPattern = "https://www.rustore.ru/catalog/app/{appId}" },
|
||||
navDeepLink { uriPattern = "https://apps.rustore.ru/app/{appId}" },
|
||||
navDeepLink { uriPattern = "https://www.rustore.ru/instruction?appName={appName}&utm_campaign={appId}" }
|
||||
)) { backStackEntry ->
|
||||
val itemId = backStackEntry.arguments?.getString("appId") ?: ""
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInHorizontally(initialOffsetX = { 1000 }) + fadeIn(),
|
||||
exit = slideOutHorizontally(targetOffsetX = { -1000 }) + fadeOut()
|
||||
) {
|
||||
DetailsScreen(itemId = itemId, navController = navController)
|
||||
}
|
||||
}
|
||||
composable("app/{packageName}/reviews") {
|
||||
val packageName = it.arguments?.getString("packageName") ?: ""
|
||||
ReviewsScreen(packageName = packageName, navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = search,
|
||||
onValueChange = {s -> search=s},
|
||||
leadingIcon = { Icon(imageVector = Icons.Default.Search, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Search
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
onSearch(search)
|
||||
}
|
||||
)
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("source code")
|
||||
Text("v1.4.0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package dev.mi6e4ka.openstore.data.api
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
object ApiClient {
|
||||
private const val BASE_URL = "https://backapi.rustore.ru/"
|
||||
|
||||
private val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.NONE
|
||||
}
|
||||
|
||||
private val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private val retrofit = Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
|
||||
val service: ApiService = retrofit.create(ApiService::class.java)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package dev.mi6e4ka.openstore.data.api
|
||||
|
||||
import dev.mi6e4ka.openstore.data.model.AppCommentsResponse
|
||||
import dev.mi6e4ka.openstore.data.model.AppRatingResponse
|
||||
import dev.mi6e4ka.openstore.data.model.DownloadLinkRequest
|
||||
import dev.mi6e4ka.openstore.data.model.DownloadLinkResponse
|
||||
import dev.mi6e4ka.openstore.data.model.ItemDetailsResponse
|
||||
import dev.mi6e4ka.openstore.data.model.SearchResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface ApiService {
|
||||
@GET("applicationData/apps")
|
||||
suspend fun search(@Query("pageSize") pageSize: Int, @Query("query") query: String): SearchResponse
|
||||
|
||||
@GET("applicationData/overallInfo/{packageName}")
|
||||
suspend fun getItemDetails(@Path("packageName") packageName: String): ItemDetailsResponse
|
||||
|
||||
@POST("applicationData/v2/download-link")
|
||||
suspend fun getAppDownloadLink(@Body request: DownloadLinkRequest) : DownloadLinkResponse
|
||||
|
||||
@GET("comment/findRating")
|
||||
suspend fun getAppRating(@Query("packageName") packageName: String) : AppRatingResponse
|
||||
|
||||
@GET("https://backapi.rustore.ru/comment/comment")
|
||||
suspend fun getAppComments(
|
||||
@Query("packageName") packageName: String,
|
||||
@Query("pageNumber") pageNumber: Int = 0,
|
||||
@Query("pageSize") pageSize: Int,
|
||||
@Query("sortBy") sortBy: String,
|
||||
) : AppCommentsResponse
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package dev.mi6e4ka.openstore.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class AppCommentsResponse(
|
||||
@SerializedName("body") val body: AppCommentsResponseBody,
|
||||
)
|
||||
|
||||
data class AppCommentsResponseBody(
|
||||
@SerializedName("content") val content: List<AppCommentsComments>,
|
||||
)
|
||||
|
||||
data class AppCommentsComments(
|
||||
@SerializedName("appRating") val appRating: Int,
|
||||
@SerializedName("firstName") val firstName: String,
|
||||
@SerializedName("commentDate") val commentDate: String,
|
||||
@SerializedName("commentText") val commentText: String,
|
||||
@SerializedName("likeCounter") val likeCounter: Int,
|
||||
@SerializedName("dislikeCounter") val dislikeCounter: Int,
|
||||
@SerializedName("updatedAt") val updatedAt: String,
|
||||
@SerializedName("devResponseDate") val devResponseDate: String,
|
||||
@SerializedName("devResponse") val devResponse: String
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package dev.mi6e4ka.openstore.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class AppRatingResponse(
|
||||
@SerializedName("body") val body: AppRatingResponseBody,
|
||||
)
|
||||
|
||||
data class AppRatingResponseBody(
|
||||
@SerializedName("ratings") val ratings: AppRatingCount,
|
||||
@SerializedName("averageUserRating") val averageUserRating: Float,
|
||||
@SerializedName("totalRatings") val totalRatings: Int,
|
||||
)
|
||||
|
||||
|
||||
data class AppRatingCount(
|
||||
@SerializedName("amountFive") val amountFive: Int,
|
||||
@SerializedName("amountFour") val amountFour: Int,
|
||||
@SerializedName("amountThree") val amountThree: Int,
|
||||
@SerializedName("amountTwo") val amountTwo: Int,
|
||||
@SerializedName("amountOne") val amountOne: Int,
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package dev.mi6e4ka.openstore.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class DownloadLinkRequest(
|
||||
@SerializedName("appId") val appId: Int,
|
||||
@SerializedName("firstInstall") val firstInstall: Boolean,
|
||||
@SerializedName("screenDensity") val screenDensity: Int,
|
||||
@SerializedName("sdkVersion") val sdkVersion: Int,
|
||||
@SerializedName("withoutSplits") val withoutSplits: Boolean,
|
||||
)
|
||||
|
||||
data class DownloadLinkResponse(
|
||||
@SerializedName("body") val body: DownloadLinkResponseBody,
|
||||
)
|
||||
|
||||
data class DownloadLinkResponseBody(
|
||||
@SerializedName("downloadUrls") val downloadUrls: List<DownloadLinkResponseItem>,
|
||||
)
|
||||
|
||||
data class DownloadLinkResponseItem(
|
||||
@SerializedName("url") val url: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package dev.mi6e4ka.openstore.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class ItemDetails(
|
||||
@SerializedName("appId") val appId: Int,
|
||||
@SerializedName("appName") val appName: String,
|
||||
@SerializedName("packageName") val packageName: String,
|
||||
@SerializedName("companyName") val companyName: String,
|
||||
@SerializedName("iconUrl") val iconUrl: String,
|
||||
@SerializedName("fullDescription") val fullDescription: String,
|
||||
@SerializedName("shortDescription") val shortDescription: String,
|
||||
@SerializedName("fileSize") val fileSize: Float,
|
||||
@SerializedName("minSdkVersion") val minSdkVersion: Int,
|
||||
@SerializedName("targetSdkVersion") val targetSdkVersion: Int,
|
||||
@SerializedName("versionName") val versionName: String,
|
||||
@SerializedName("versionCode") val versionCode: Long,
|
||||
@SerializedName("fileUrls") val fileUrls: List<Files>,
|
||||
@SerializedName("aggregatorInfo") val aggregatorInfo: AggregatorInfo?,
|
||||
@SerializedName("roundedDownloadsText") val roundedDownloadsText: String,
|
||||
@SerializedName("ageLegal") val ageLegal: String,
|
||||
@SerializedName("whatsNew") val whatsNew: String,
|
||||
)
|
||||
|
||||
data class Files(
|
||||
@SerializedName("fileUrl") val fileUrl: String,
|
||||
@SerializedName("ordinal") val ordinal: Int,
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("orientation") val orientation: String,
|
||||
)
|
||||
|
||||
data class AggregatorInfo(
|
||||
@SerializedName("companyName") val companyName: String,
|
||||
@SerializedName("source") val source: String,
|
||||
)
|
||||
|
||||
data class ItemDetailsResponse(
|
||||
@SerializedName("code") val code: String,
|
||||
@SerializedName("message") val message: String?,
|
||||
@SerializedName("body") val body: ItemDetails,
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package dev.mi6e4ka.openstore.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class SearchResult(
|
||||
@SerializedName("appId") val appId: Int,
|
||||
@SerializedName("appName") val appName: String,
|
||||
@SerializedName("packageName") val packageName: String,
|
||||
@SerializedName("iconUrl") val iconUrl: String,
|
||||
)
|
||||
|
||||
data class SearchResponseBody(
|
||||
@SerializedName("content") val content: List<SearchResult>
|
||||
)
|
||||
|
||||
data class SearchResponse(
|
||||
@SerializedName("code") val code: String,
|
||||
@SerializedName("message") val message: String?,
|
||||
@SerializedName("body") val body: SearchResponseBody
|
||||
)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package dev.mi6e4ka.openstore.data.repository
|
||||
|
||||
import dev.mi6e4ka.openstore.data.api.ApiClient
|
||||
import dev.mi6e4ka.openstore.data.model.AppCommentsComments
|
||||
import dev.mi6e4ka.openstore.data.model.AppRatingResponseBody
|
||||
import dev.mi6e4ka.openstore.data.model.DownloadLinkRequest
|
||||
import dev.mi6e4ka.openstore.data.model.ItemDetails
|
||||
import dev.mi6e4ka.openstore.data.model.SearchResult
|
||||
|
||||
class SearchRepository {
|
||||
private val apiService = ApiClient.service
|
||||
|
||||
suspend fun search(query: String): List<SearchResult> {
|
||||
return apiService.search(20, query).body.content
|
||||
}
|
||||
|
||||
suspend fun getItemDetails(id: String): ItemDetails {
|
||||
return apiService.getItemDetails(id).body
|
||||
}
|
||||
|
||||
suspend fun getAppDownloadLink(id: Int): String {
|
||||
val req = DownloadLinkRequest(appId = id, firstInstall = true, screenDensity = 420, sdkVersion = 36, withoutSplits = true)
|
||||
return apiService.getAppDownloadLink(req).body.downloadUrls[0].url
|
||||
}
|
||||
|
||||
suspend fun getAppRating(packageName: String): AppRatingResponseBody {
|
||||
return apiService.getAppRating(packageName).body
|
||||
}
|
||||
|
||||
suspend fun getAppComments(packageName: String, pageSize: Int, sortBy: String): List<AppCommentsComments> {
|
||||
return apiService.getAppComments(packageName, pageSize = pageSize, sortBy = sortBy).body.content
|
||||
}
|
||||
}
|
||||
14
app/src/main/java/dev/mi6e4ka/openstore/di/AppModule.kt
Normal file
14
app/src/main/java/dev/mi6e4ka/openstore/di/AppModule.kt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package dev.mi6e4ka.openstore.di
|
||||
|
||||
import dev.mi6e4ka.openstore.data.repository.SearchRepository
|
||||
import dev.mi6e4ka.openstore.internal.installer.UninstallerEventFlow
|
||||
|
||||
object AppModule {
|
||||
val searchRepository: SearchRepository by lazy {
|
||||
SearchRepository()
|
||||
}
|
||||
|
||||
val uninstallerEventFlow: UninstallerEventFlow by lazy {
|
||||
UninstallerEventFlow()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package dev.mi6e4ka.openstore.internal.installer
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.IOException
|
||||
|
||||
class ApkInstaller(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private val okHttpClient = OkHttpClient()
|
||||
}
|
||||
|
||||
suspend fun downloadApkWithProgress(
|
||||
url: String,
|
||||
onProgress: (percent: Int, downloaded: Long, total: Long) -> Unit
|
||||
): File = withContext(Dispatchers.IO) {
|
||||
val fileName = url.split("/").last()
|
||||
|
||||
val apkFile = getApkFile(fileName)
|
||||
|
||||
// Проверяем, не скачан ли уже файл
|
||||
if (isApkAlreadyDownloaded(fileName)) {
|
||||
return@withContext apkFile
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Ошибка загрузки: ${response.code}")
|
||||
}
|
||||
|
||||
val responseBody = response.body
|
||||
if (responseBody == null) {
|
||||
throw IOException("Пустой ответ от сервера")
|
||||
}
|
||||
|
||||
downloadToFileWithProgress(responseBody, apkFile, onProgress)
|
||||
}
|
||||
|
||||
apkFile
|
||||
}
|
||||
|
||||
private fun downloadToFileWithProgress(
|
||||
responseBody: ResponseBody,
|
||||
file: File,
|
||||
onProgress: (percent: Int, downloaded: Long, total: Long) -> Unit
|
||||
) {
|
||||
val totalBytes = responseBody.contentLength()
|
||||
var downloadedBytes = 0L
|
||||
|
||||
responseBody.byteStream().use { inputStream ->
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
|
||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
downloadedBytes += bytesRead
|
||||
|
||||
// Вычисляем прогресс
|
||||
val progress = if (totalBytes > 0) {
|
||||
(downloadedBytes * 100 / totalBytes).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
// Отправляем прогресс
|
||||
onProgress(progress, downloadedBytes, totalBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isApkAlreadyDownloaded(fileName: String): Boolean {
|
||||
val apkFile = getApkFile(fileName)
|
||||
return apkFile.exists() && apkFile.length() > 0
|
||||
}
|
||||
|
||||
fun getApkFile(fileName: String): File {
|
||||
return File(context.externalCacheDir, fileName)
|
||||
}
|
||||
|
||||
fun deleteApkFile(fileName: String) {
|
||||
val apkFile = getApkFile(fileName)
|
||||
if (apkFile.exists()) {
|
||||
apkFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkFileSize(fileName: String): Long {
|
||||
return getApkFile(fileName).length()
|
||||
}
|
||||
|
||||
private val installer = context.packageManager.packageInstaller
|
||||
private val flags = PendingIntent.FLAG_MUTABLE
|
||||
private val intent = Intent(context, PackageInstallerStatusReceiver::class.java)
|
||||
private val sessionParams =
|
||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
|
||||
// //sdkAbove(sdk = Build.VERSION_CODES.S) {
|
||||
// setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||
// //}
|
||||
// //sdkAbove(sdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
// setRequestUpdateOwnership(true)
|
||||
// //}
|
||||
}
|
||||
|
||||
fun installApk(context: Context, apkFile: File, installerCallback: PackageInstaller.SessionCallback) {
|
||||
val id = installer.createSession(sessionParams)
|
||||
installer.registerSessionCallback(
|
||||
installerCallback,
|
||||
Handler(Looper.getMainLooper())
|
||||
)
|
||||
|
||||
val session = installer.openSession(id)
|
||||
|
||||
session.use { activeSession ->
|
||||
val sizeBytes = apkFile.length()
|
||||
apkFile.inputStream().use { fileStream ->
|
||||
activeSession.openWrite(apkFile.name, 0, sizeBytes).use { outputStream ->
|
||||
fileStream.copyTo(outputStream)
|
||||
activeSession.fsync(outputStream)
|
||||
}
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, id, intent, flags)
|
||||
activeSession.commit(pendingIntent.intentSender)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package dev.mi6e4ka.openstore.internal.installer
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.widget.Toast
|
||||
import dev.mi6e4ka.openstore.di.AppModule
|
||||
|
||||
class PackageInstallerStatusReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
|
||||
val isUninstall = intent.getBooleanExtra(ACTION_UNINSTALL, false)
|
||||
|
||||
when (status) {
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
Toast.makeText(context, if (isUninstall) "Удалено" else "Установлено", Toast.LENGTH_SHORT).show()
|
||||
if (isUninstall) {
|
||||
AppModule.uninstallerEventFlow.trigger()
|
||||
}
|
||||
}
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
|
||||
promptIntent?.let {
|
||||
it.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
|
||||
it.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, "com.android.vending")
|
||||
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
context.startActivity(it)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(context, if (isUninstall) "Не удалось удалить" else "Не удалось установить", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_UNINSTALL = "action_uninstall"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package dev.mi6e4ka.openstore.internal.installer
|
||||
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
|
||||
class UninstallerEventFlow() {
|
||||
private val _events = Channel<Unit>(Channel.BUFFERED)
|
||||
val events = _events.receiveAsFlow()
|
||||
|
||||
fun trigger() {
|
||||
_events.trySend(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package dev.mi6e4ka.openstore.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
|
||||
@Composable
|
||||
fun AppIcon(
|
||||
imageUrl: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = "Icon",
|
||||
modifier = modifier,
|
||||
loading = {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
},
|
||||
error = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
imageVector = Icons.Filled.Error,
|
||||
contentDescription = "Error"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
171
app/src/main/java/dev/mi6e4ka/openstore/ui/components/Gallery.kt
Normal file
171
app/src/main/java/dev/mi6e4ka/openstore/ui/components/Gallery.kt
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package dev.mi6e4ka.openstore.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import dev.mi6e4ka.openstore.data.model.Files
|
||||
|
||||
@Composable
|
||||
fun GalleryWithPager(images: List<Files>) {
|
||||
var selectedImageIndex by remember { mutableStateOf<Int?>(null) }
|
||||
|
||||
if (selectedImageIndex != null) {
|
||||
Dialog(
|
||||
onDismissRequest = { selectedImageIndex = null },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
FullScreenGallery(
|
||||
images = images,
|
||||
initialIndex = selectedImageIndex ?: 0,
|
||||
onDismiss = { selectedImageIndex = null }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier.height(250.dp)
|
||||
) {
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
itemsIndexed(images) { i, file ->
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
AsyncImage(
|
||||
model = file.fileUrl,
|
||||
contentDescription = null,
|
||||
onSuccess = { isLoading = false },
|
||||
onError = { isLoading = false },
|
||||
onLoading = { isLoading = true },
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.fillMaxHeight()
|
||||
.clickable(onClick = {
|
||||
selectedImageIndex = i
|
||||
}),
|
||||
contentScale = ContentScale.FillHeight
|
||||
)
|
||||
if (isLoading) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(9f / 16f),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun FullScreenGallery(
|
||||
images: List<Files>,
|
||||
initialIndex: Int,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val pagerState = rememberPagerState(initialPage = initialIndex, pageCount = { images.size })
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.clickable(onClick = onDismiss),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
//contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) { page ->
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
// .padding(horizontal = ((
|
||||
// (pagerState.currentPage - page) + pagerState
|
||||
// .currentPageOffsetFraction
|
||||
// ).absoluteValue * 0).dp)
|
||||
// .graphicsLayer {
|
||||
// val pageOffset = (
|
||||
// (pagerState.currentPage - page) + pagerState
|
||||
// .currentPageOffsetFraction
|
||||
// ).absoluteValue
|
||||
// alpha = lerp(
|
||||
// start = 0.5f,
|
||||
// stop = 1f,
|
||||
// fraction = 1f - pageOffset.coerceIn(0f, 1f)
|
||||
// )
|
||||
// }
|
||||
) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(images[page].fileUrl),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(onClick = onDismiss),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
repeat(pagerState.pageCount) { iteration ->
|
||||
val color = if (pagerState.currentPage == iteration) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.primaryContainer
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.size(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package dev.mi6e4ka.openstore.ui.nav
|
||||
|
|
@ -1 +0,0 @@
|
|||
package dev.mi6e4ka.openstore.ui.nav
|
||||
|
|
@ -0,0 +1,493 @@
|
|||
package dev.mi6e4ka.openstore.ui.screen.details
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
|
||||
import androidx.compose.material.icons.filled.ArrowCircleUp
|
||||
import androidx.compose.material.icons.filled.OpenInBrowser
|
||||
import androidx.compose.material.icons.filled.SupervisedUserCircle
|
||||
import androidx.compose.material.icons.rounded.Android
|
||||
import androidx.compose.material.icons.rounded.Api
|
||||
import androidx.compose.material.icons.rounded.Download
|
||||
import androidx.compose.material.icons.rounded.Security
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.AssistChipDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dev.mi6e4ka.openstore.di.AppModule
|
||||
import dev.mi6e4ka.openstore.internal.installer.ApkInstaller
|
||||
import dev.mi6e4ka.openstore.ui.components.AppIcon
|
||||
import dev.mi6e4ka.openstore.ui.components.GalleryWithPager
|
||||
import kotlinx.coroutines.flow.update
|
||||
import androidx.core.net.toUri
|
||||
import dev.mi6e4ka.openstore.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun DetailsScreen(
|
||||
itemId: String,
|
||||
navController: NavController = rememberNavController()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val apkInstaller = ApkInstaller(context)
|
||||
|
||||
//val viewModel = remember (itemId) { DetailsViewModel(itemId, apkInstaller) }
|
||||
val viewModel : DetailsViewModel = viewModel(
|
||||
factory = DetailsViewModelFactory(itemId, apkInstaller)
|
||||
)
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var showAppDescriptionSheet by remember { mutableStateOf(false) }
|
||||
// val appDescriptionSheetState = rememberModalBottomSheetState(
|
||||
// skipPartiallyExpanded = true
|
||||
// )
|
||||
|
||||
LaunchedEffect(itemId) {
|
||||
viewModel.loadDetails(context, itemId)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
println("sub to flow")
|
||||
AppModule.uninstallerEventFlow.events.collect {
|
||||
println("FLAG CATCHED!")
|
||||
viewModel.updateInstallStatus(false)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
val clipboard = context.getSystemService(ClipboardManager::class.java)
|
||||
val link = AppModule.searchRepository.getAppDownloadLink(state.item?.appId ?: 0)
|
||||
val clip = ClipData.newPlainText("", link)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
}) {
|
||||
Icon(painterResource(R.drawable.apk_install_24px), null)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "https://rustore.ru/catalog/app/${state.item?.packageName ?: ""}".toUri())
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Icon(Icons.Default.OpenInBrowser, null)
|
||||
}
|
||||
}
|
||||
) }
|
||||
) {
|
||||
innerPadding ->
|
||||
Column(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)) {
|
||||
if (showAppDescriptionSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {showAppDescriptionSheet = false},
|
||||
Modifier.systemBarsPadding()
|
||||
) {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState()).padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("О приложении", fontWeight = FontWeight.Bold, fontSize = 18.sp)
|
||||
Text(state.item?.fullDescription ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.isLoading) {
|
||||
LinearProgressIndicator(modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.fillMaxWidth())
|
||||
} else if (state.error != null) {
|
||||
Text("Ошибка: ${state.error}", modifier = Modifier.padding(16.dp))
|
||||
} else {
|
||||
val item = state.item
|
||||
if (item != null) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (item.aggregatorInfo == null)
|
||||
Color(0, 255, 0, 35)
|
||||
else Color(255, 187, 0, 35)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "https://codeberg.org/mi6e4ka/openstore/src/branch/main/SAFETY.md".toUri())
|
||||
context.startActivity(intent)
|
||||
})
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(15.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Security,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
if (item.aggregatorInfo != null) {
|
||||
Text("Предоставлено ${item.aggregatorInfo.source}")
|
||||
} else {
|
||||
Text("Предоставлено RuStore")
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(92.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val animatedSize by animateDpAsState(
|
||||
targetValue = if (state.isDownloading) 80.dp else 92.dp,
|
||||
animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing),
|
||||
label = "sizeAnimation"
|
||||
)
|
||||
val animatedCorner by animateDpAsState(
|
||||
targetValue = if (state.isDownloading) 80.dp / 2 else 12.dp,
|
||||
animationSpec = spring(dampingRatio = 0.6f),
|
||||
label = "cornerAnimation"
|
||||
)
|
||||
if (state.installStatus !is InstallStatus.Pending) {
|
||||
CircularProgressIndicator(
|
||||
progress = { state.downloadProgress / 100f },
|
||||
modifier = Modifier
|
||||
.size(128.dp)
|
||||
.alpha(if (state.isDownloading) 1f else 0f),
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(128.dp)
|
||||
.alpha(if (state.isDownloading) 1f else 0f),
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
AppIcon(
|
||||
item.iconUrl, modifier = Modifier
|
||||
.size(animatedSize)
|
||||
.clip(RoundedCornerShape(animatedCorner))
|
||||
)
|
||||
}
|
||||
Column(
|
||||
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)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (!state.isInstalled || state.isDownloading) {
|
||||
//FilledTonalButton(onClick = {}, modifier = Modifier.weight(1f)) { Text("Copy apk link") }
|
||||
Button(onClick = {
|
||||
viewModel.downloadAndInstallApk(context)
|
||||
}, modifier = Modifier.weight(1f), enabled = !state.isDownloading) {
|
||||
if (state.installStatus is InstallStatus.Pending) {
|
||||
Text("Установка...")
|
||||
} else if (state.isDownloading) {
|
||||
Text("Загрузка... ${state.downloadCurrentBytes/1024/1024}/${state.downloadTotalBytes/1024/1024}МБ")
|
||||
} else {
|
||||
Text("Установить")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FilledTonalButton(onClick = {
|
||||
viewModel.deleteApplication(context, itemId)
|
||||
}, modifier = Modifier.weight(1f)) { Text("Удалить") }
|
||||
if (state.isNeedUpgrade) {
|
||||
Button(onClick = {
|
||||
viewModel.downloadAndInstallApk(context)
|
||||
}, modifier = Modifier.weight(1f)) { Text("Обновить") }
|
||||
} else {
|
||||
Button(onClick = {
|
||||
viewModel.runApplication(context, itemId)
|
||||
}, modifier = Modifier.weight(1f)) { Text("Открыть") }
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(bottom = 4.dp, top = 2.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
//InfoBadge("%.1f MB".format(item.fileSize/1024/1024), "Вес")
|
||||
// InfoBadge(, "загрузок")
|
||||
// InfoBadge("${getAndroidVersion(item.minSdkVersion)}+", "Android",
|
||||
// Modifier
|
||||
// .clip(RoundedCornerShape(12.dp))
|
||||
// .clickable(onClick = {
|
||||
//
|
||||
// }))
|
||||
// InfoBadge(item.ageLegal, "возраст")
|
||||
// InfoBadge(item.versionName, "версия")
|
||||
AssistChip(
|
||||
onClick = {
|
||||
Toast.makeText(context, "MinSdkVersion ${item.minSdkVersion}\nTargetSdkVersion ${item.targetSdkVersion}", Toast.LENGTH_SHORT).show() },
|
||||
label = {Text("Android ${getAndroidVersion(item.minSdkVersion)} +")},
|
||||
leadingIcon = {Icon(imageVector = Icons.Rounded.Android, contentDescription = null)}
|
||||
)
|
||||
AssistChip(
|
||||
onClick = {},
|
||||
label = {Text(item.roundedDownloadsText)},
|
||||
leadingIcon = {Icon(imageVector = Icons.Rounded.Download, contentDescription = null)}
|
||||
)
|
||||
AssistChip(
|
||||
onClick = {},
|
||||
label = {Text(item.versionName)},
|
||||
leadingIcon = {Icon(imageVector = Icons.Default.ArrowCircleUp, contentDescription = null)}
|
||||
)
|
||||
AssistChip(
|
||||
onClick = {},
|
||||
label = {Text(item.ageLegal)},
|
||||
leadingIcon = {Icon(imageVector = Icons.Default.SupervisedUserCircle, contentDescription = null)}
|
||||
)
|
||||
}
|
||||
GalleryWithPager(item.fileUrls)
|
||||
}
|
||||
var isWhatsNewExpanded by remember { mutableStateOf(false) }
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {isWhatsNewExpanded = !isWhatsNewExpanded}
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(16.dp)
|
||||
.animateContentSize(
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
)
|
||||
) {
|
||||
Row {
|
||||
Text("Что нового?", fontWeight = FontWeight.Bold, fontSize = 18.sp)
|
||||
}
|
||||
|
||||
Text(item.whatsNew, maxLines = if (isWhatsNewExpanded) Int.MAX_VALUE else 4, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Column(
|
||||
Modifier
|
||||
.padding(bottom = 8.dp)
|
||||
.clickable(interactionSource = interactionSource, indication = null) {
|
||||
showAppDescriptionSheet = true
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("О приложении", fontWeight = FontWeight.Bold, fontSize = 18.sp)
|
||||
IconButton(onClick = {showAppDescriptionSheet = true}) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Rounded.ArrowForward, contentDescription = null)
|
||||
}
|
||||
}
|
||||
Text(text = item.shortDescription)
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = {navController.navigate("app/${item.packageName}/reviews"){restoreState=true} })
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Рейтинг и отзывы", fontWeight = FontWeight.Bold, fontSize = 18.sp)
|
||||
// FilledTonalIconButton(onClick = {}) {
|
||||
// Icon(imageVector = Icons.Rounded.ArrowForward, contentDescription = null)
|
||||
// }
|
||||
}
|
||||
Row(modifier = Modifier.padding(start = 20.dp, end = 20.dp, bottom = 16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("5", fontWeight = FontWeight.Bold)
|
||||
LinearProgressIndicator(progress = {
|
||||
((state.rating?.ratings?.amountFive ?: 0) / 100f)
|
||||
}, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("4", fontWeight = FontWeight.Bold)
|
||||
LinearProgressIndicator(progress = {
|
||||
((state.rating?.ratings?.amountFour ?: 0) / 100f)
|
||||
}, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("3", fontWeight = FontWeight.Bold)
|
||||
LinearProgressIndicator(progress = {
|
||||
((state.rating?.ratings?.amountThree ?: 0) / 100f)
|
||||
}, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("2", fontWeight = FontWeight.Bold)
|
||||
LinearProgressIndicator(progress = {
|
||||
((state.rating?.ratings?.amountTwo ?: 0) / 100f)
|
||||
}, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("1", fontWeight = FontWeight.Bold)
|
||||
LinearProgressIndicator(progress = {
|
||||
((state.rating?.ratings?.amountOne ?: 0) / 100f)
|
||||
}, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(start=24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("${state.rating?.averageUserRating ?: "?"}", fontWeight = FontWeight.Bold, fontSize = 24.sp)
|
||||
Text("${state.rating?.totalRatings ?: "?"}", fontSize = 16.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(item.packageName, color = Color.Gray, fontSize = 14.sp)
|
||||
}
|
||||
//Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAndroidVersion(apiLevel: Int): String {
|
||||
val versions = mapOf(
|
||||
1 to "1.0",
|
||||
2 to "1.1",
|
||||
3 to "1.5",
|
||||
4 to "1.6",
|
||||
5 to "2.0",
|
||||
6 to "2.0.1",
|
||||
7 to "2.1",
|
||||
8 to "2.2",
|
||||
9 to "2.3",
|
||||
10 to "2.3.3",
|
||||
11 to "3.0",
|
||||
12 to "3.1",
|
||||
13 to "3.2",
|
||||
14 to "4.0",
|
||||
15 to "4.0.3",
|
||||
16 to "4.1",
|
||||
17 to "4.2",
|
||||
18 to "4.3",
|
||||
19 to "4.4",
|
||||
20 to "4.4W", // Wear
|
||||
21 to "5.0",
|
||||
22 to "5.1",
|
||||
23 to "6.0",
|
||||
24 to "7.0",
|
||||
25 to "7.1",
|
||||
26 to "8.0",
|
||||
27 to "8.1",
|
||||
28 to "9.0",
|
||||
29 to "10",
|
||||
30 to "11",
|
||||
31 to "12",
|
||||
32 to "12L", // Android 12L
|
||||
33 to "13",
|
||||
34 to "14",
|
||||
35 to "15",
|
||||
36 to "16",
|
||||
37 to "17"
|
||||
)
|
||||
|
||||
return versions[apiLevel] ?: "? (API $apiLevel)"
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
package dev.mi6e4ka.openstore.ui.screen.details
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.mi6e4ka.openstore.data.model.AppCommentsResponseBody
|
||||
import dev.mi6e4ka.openstore.data.model.AppRatingResponseBody
|
||||
import dev.mi6e4ka.openstore.data.model.ItemDetails
|
||||
import dev.mi6e4ka.openstore.di.AppModule
|
||||
import dev.mi6e4ka.openstore.internal.installer.ApkInstaller
|
||||
import dev.mi6e4ka.openstore.internal.installer.PackageInstallerStatusReceiver
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
data class DetailsState(
|
||||
val isLoading: Boolean = false,
|
||||
val item: ItemDetails? = null,
|
||||
val rating: AppRatingResponseBody? = null,
|
||||
val error: String? = null,
|
||||
val downloadLink: String? = null,
|
||||
val installStatus: InstallStatus = InstallStatus.Idle,
|
||||
val isDownloading: Boolean = false,
|
||||
val downloadProgress: Int = 0,
|
||||
val downloadCurrentBytes: Long = 0,
|
||||
val downloadTotalBytes: Long = 0,
|
||||
val isInstalled: Boolean = false,
|
||||
val isNeedUpgrade: Boolean = false,
|
||||
)
|
||||
|
||||
sealed class InstallStatus {
|
||||
object Idle : InstallStatus()
|
||||
object Pending : InstallStatus()
|
||||
object Success : InstallStatus()
|
||||
}
|
||||
|
||||
class DetailsViewModel(private val itemId: String, private val apkInstaller: ApkInstaller) : ViewModel() {
|
||||
private val _state = MutableStateFlow(DetailsState())
|
||||
val state: StateFlow<DetailsState> = _state.asStateFlow()
|
||||
|
||||
fun loadDetails(context: Context, itemId: String) {
|
||||
if (_state.value.item != null) {
|
||||
return
|
||||
}
|
||||
Log.d(null, "Initial loading")
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val item = AppModule.searchRepository.getItemDetails(itemId)
|
||||
//
|
||||
val packageInfo = getPackageInfo(context, itemId)
|
||||
_state.value = DetailsState(isLoading = true, isInstalled = packageInfo != null)
|
||||
if (packageInfo != null) {
|
||||
if (packageInfo.versionCode < item.versionCode) {
|
||||
_state.update { it.copy(isNeedUpgrade = true) }
|
||||
}
|
||||
}
|
||||
//
|
||||
val downloadLink = AppModule.searchRepository.getAppDownloadLink(item.appId)
|
||||
val rating = AppModule.searchRepository.getAppRating(itemId)
|
||||
_state.update{ it.copy(item = item, rating = rating, downloadLink = downloadLink, isLoading = false) }
|
||||
Log.d("loading", "LOADED COMMENTS+RATING: ${state.value.rating}")
|
||||
} catch (e: Exception) {
|
||||
_state.update { it.copy(error = e.message, isLoading = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadAndInstallApk(context: Context) {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isDownloading = true) }
|
||||
Log.d(null, "download apk " + state.value.downloadLink)
|
||||
val file = apkInstaller.downloadApkWithProgress(state.value.downloadLink ?: "") { percent, downloaded, total ->
|
||||
_state.update { it.copy(downloadProgress = percent, downloadCurrentBytes = downloaded, downloadTotalBytes = total) }
|
||||
}
|
||||
Log.d(null, "file: " + file.toUri())
|
||||
try {
|
||||
val apkFile = file
|
||||
installApk(context, apkFile)
|
||||
// val uri = FileProvider.getUriForFile(
|
||||
// context,
|
||||
// "${context.packageName}.provider",
|
||||
// apkFile
|
||||
// )
|
||||
//
|
||||
// val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
// setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
// addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
// addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
// }
|
||||
// launcher.launch(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(null, "ERROR: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun installApk(context: Context, apkFile: File) {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(installStatus = InstallStatus.Pending) }
|
||||
val installerCallback = object : android.content.pm.PackageInstaller.SessionCallback() {
|
||||
override fun onCreated(sessionId: Int) {}
|
||||
override fun onBadgingChanged(sessionId: Int) {}
|
||||
override fun onActiveChanged(sessionId: Int, active: Boolean) {}
|
||||
override fun onProgressChanged(sessionId: Int, progress: Float) {}
|
||||
override fun onFinished(sessionId: Int, success: Boolean) {
|
||||
_state.update { it.copy(installStatus = InstallStatus.Success, isDownloading = false)}
|
||||
if (success) {
|
||||
_state.update { it.copy(isInstalled = true, isNeedUpgrade = false) }
|
||||
apkInstaller.deleteApkFile(apkFile.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
apkInstaller.installApk(context, apkFile, installerCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPackageInfo(context: Context, packageName: String): PackageInfo? {
|
||||
Log.d(null, "try to check package $packageName")
|
||||
return try {
|
||||
val packageInfo : PackageInfo
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageInfo = context.packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packageInfo = context.packageManager.getPackageInfo(packageName, 0)
|
||||
}
|
||||
Log.d(null, "try")
|
||||
packageInfo
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Log.d(null, "false")
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
fun runApplication(context: Context, packageName: String) {
|
||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
if (launchIntent != null) {
|
||||
context.startActivity(launchIntent)
|
||||
} else {
|
||||
Toast.makeText(context, "Не удалось открыть приложение", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
fun deleteApplication(context: Context, packageName: String) {
|
||||
val intent = Intent(context, PackageInstallerStatusReceiver::class.java)
|
||||
intent.putExtra(PackageInstallerStatusReceiver.ACTION_UNINSTALL, true)
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, -1, intent, PendingIntent.FLAG_MUTABLE)
|
||||
context.packageManager.packageInstaller.uninstall(packageName, pendingIntent.intentSender)
|
||||
}
|
||||
|
||||
fun updateInstallStatus(isInstalled: Boolean) {
|
||||
_state.update { it.copy(isInstalled = isInstalled) }
|
||||
}
|
||||
}
|
||||
|
||||
class DetailsViewModelFactory(
|
||||
private val itemId: String,
|
||||
private val apkInstaller: ApkInstaller
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(DetailsViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return DetailsViewModel(itemId, apkInstaller) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package dev.mi6e4ka.openstore.ui.screen.results
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dev.mi6e4ka.openstore.di.AppModule
|
||||
import dev.mi6e4ka.openstore.ui.components.AppIcon
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ResultsScreen(
|
||||
query: String,
|
||||
navController: NavController = rememberNavController(),
|
||||
) {
|
||||
//val viewModel = remember(query) { ResultsViewModel(query) }
|
||||
val viewModel : ResultsViewModel = viewModel(
|
||||
factory = ResultsViewModelFactory(query)
|
||||
)
|
||||
val state by viewModel.state.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(query) {
|
||||
viewModel.search()
|
||||
}
|
||||
Scaffold(
|
||||
topBar = { TopAppBar(
|
||||
title = {Text("Поиск")},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
) }
|
||||
) { innerPadding ->
|
||||
Column(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
|
||||
if (state.isLoading) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp)
|
||||
)
|
||||
} else if (state.error != null) {
|
||||
Text("Ошибка: ${state.error}", modifier = Modifier.padding(16.dp))
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(state.results) { item ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
//.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = { navController.navigate("app/${item.packageName}") })
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppIcon(item.iconUrl, modifier = Modifier.size(60.dp).clip(RoundedCornerShape(16.dp)))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f).padding(start = 16.dp,end = 4.dp)
|
||||
) {
|
||||
Text(text = item.appName, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(text = item.packageName, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
// FilledTonalIconButton (
|
||||
// onClick = {
|
||||
// scope.launch {
|
||||
// val clipboard = context.getSystemService(ClipboardManager::class.java)
|
||||
// val link = AppModule.searchRepository.getAppDownloadLink(item.appId)
|
||||
// val clip = ClipData.newPlainText("", link)
|
||||
// clipboard.setPrimaryClip(clip)
|
||||
// }
|
||||
// }
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Filled.Link,
|
||||
// contentDescription = "Copy app download link"
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package dev.mi6e4ka.openstore.ui.screen.results
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.mi6e4ka.openstore.data.model.SearchResult
|
||||
import dev.mi6e4ka.openstore.di.AppModule
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class ResultsState(
|
||||
val isLoading: Boolean = false,
|
||||
val results: List<SearchResult> = emptyList(),
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
class ResultsViewModel(private val query: String) : ViewModel() {
|
||||
private val _state = MutableStateFlow(ResultsState())
|
||||
val state: StateFlow<ResultsState> = _state.asStateFlow()
|
||||
|
||||
fun search() {
|
||||
if (_state.value.results.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
_state.value = ResultsState(isLoading = true)
|
||||
println("search...")
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val results = AppModule.searchRepository.search(query)
|
||||
_state.value = ResultsState(results = results)
|
||||
} catch (e: Exception) {
|
||||
_state.value = ResultsState(error = e.message)
|
||||
println("Search error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResultsViewModelFactory(
|
||||
private val query: String
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(ResultsViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return ResultsViewModel(query) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
package dev.mi6e4ka.openstore.ui.screen.reviews
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.StarRate
|
||||
import androidx.compose.material.icons.filled.ThumbDown
|
||||
import androidx.compose.material.icons.filled.ThumbDownOffAlt
|
||||
import androidx.compose.material.icons.filled.ThumbUp
|
||||
import androidx.compose.material.icons.filled.ThumbUpAlt
|
||||
import androidx.compose.material.icons.filled.ThumbUpOffAlt
|
||||
import androidx.compose.material.icons.outlined.Star
|
||||
import androidx.compose.material.icons.outlined.StarBorder
|
||||
import androidx.compose.material.icons.outlined.StarRate
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
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.rememberCoroutineScope
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dev.mi6e4ka.openstore.data.model.AppCommentsComments
|
||||
import dev.mi6e4ka.openstore.di.AppModule
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
enum class ReviewsFilters(val value: String, val displayName: String) {
|
||||
NewFirst("NEW_FIRST", "Новые"),
|
||||
UsefulFirst("USEFUL_FIRST", "Полезные"),
|
||||
PositiveFirst("POSITIVE_FIRST", "Позитивные"),
|
||||
NegativeFirst("NEGATIVE_FIRST", "Негативные")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReviewsScreen(packageName: String, navController: NavController = rememberNavController()) {
|
||||
var reviews: List<AppCommentsComments>? by remember { mutableStateOf(null) }
|
||||
var selectedFilter by remember { mutableStateOf(ReviewsFilters.NewFirst) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(selectedFilter) {
|
||||
isLoading = true
|
||||
scope.launch {
|
||||
reviews = try {
|
||||
AppModule.searchRepository.getAppComments(packageName, 50, selectedFilter.value)
|
||||
} catch (e: Exception) {
|
||||
listOf(AppCommentsComments(
|
||||
appRating = 1,
|
||||
firstName = "Семен",
|
||||
commentDate = "2025-08-30 10:25:20.000",
|
||||
commentText = "Не работает этот ваш опенстор, удаляю",
|
||||
likeCounter = 100,
|
||||
dislikeCounter = 1,
|
||||
updatedAt = "2025-08-30 10:25:20.000",
|
||||
devResponse = "",
|
||||
devResponseDate = "",
|
||||
))
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = { TopAppBar(
|
||||
title = {Text("Отзывы")},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
) },
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
Modifier.padding(innerPadding).padding(horizontal = 16.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
LinearProgressIndicator(Modifier.fillMaxWidth())
|
||||
}
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
Modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ReviewsFilters.entries.forEach { filter ->
|
||||
FilterChip(
|
||||
selected = filter == selectedFilter,
|
||||
onClick = { selectedFilter = filter },
|
||||
label = {Text(filter.displayName)},
|
||||
leadingIcon = if (selectedFilter == filter) {
|
||||
{ Icon(Icons.Default.Check, contentDescription = null) }
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
if ((reviews?.size ?: 0) == 0 && !isLoading) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
Text("Отзывов пока нет")
|
||||
}
|
||||
}
|
||||
}
|
||||
items(reviews ?: listOf()) { review ->
|
||||
Card(
|
||||
Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(review.firstName, fontWeight = FontWeight.Bold)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(formatTimestamp(review.commentDate), color = Color.Gray)
|
||||
}
|
||||
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
repeat(review.appRating) {
|
||||
Icon(Icons.Filled.Star, null, Modifier.size(16.dp))
|
||||
}
|
||||
repeat(5 - review.appRating) {
|
||||
Icon(Icons.Outlined.StarBorder, null, Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(review.commentText)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(imageVector = Icons.Default.ThumbUpOffAlt, contentDescription = null, Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("${review.likeCounter}")
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(imageVector = Icons.Default.ThumbDownOffAlt, contentDescription = null, Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("${review.dislikeCounter}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatTimestamp(timestamp: String): String {
|
||||
val inputFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||
val outputFormat = SimpleDateFormat("d MMMM yyyy", Locale.getDefault())
|
||||
|
||||
val date = inputFormat.parse(timestamp)
|
||||
return outputFormat.format(date ?: "")
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
package dev.mi6e4ka.openstore.ui.screen.search
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.verify.domain.DomainVerificationManager
|
||||
import android.content.pm.verify.domain.DomainVerificationUserState
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DomainVerification
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Verified
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dev.mi6e4ka.openstore.BuildConfig
|
||||
import dev.mi6e4ka.openstore.R
|
||||
import java.net.URLEncoder
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchScreen(navController: NavController = rememberNavController()) {
|
||||
var search by remember { mutableStateOf("") }
|
||||
var isDomainsVerified by remember { mutableStateOf(true) }
|
||||
val context = LocalContext.current
|
||||
|
||||
OnResumeEffect {
|
||||
isDomainsVerified = true
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val manager = context.getSystemService(DomainVerificationManager::class.java)
|
||||
val states = manager.getDomainVerificationUserState(context.packageName)
|
||||
states?.hostToStateMap?.forEach { state ->
|
||||
Log.d("state", "$state")
|
||||
if (state.value != 1) {
|
||||
isDomainsVerified = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = { TopAppBar(title = {Text(stringResource(R.string.app_name))}) },
|
||||
) { innerPadding ->
|
||||
// if (showDialog) {
|
||||
// AlertDialog(
|
||||
// onDismissRequest = {showDialog=false},
|
||||
// title = { Text("Добавьте ссылки RuStore") },
|
||||
// text = { Text("Чтобы ") },
|
||||
// confirmButton = {
|
||||
// TextButton(onClick = {
|
||||
// val intent = Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
||||
// .apply { data = Uri.fromParts("package", context.packageName, null) }
|
||||
// context.startActivity(intent)
|
||||
// }) {
|
||||
// Text("Подтвердить")
|
||||
// }
|
||||
// },
|
||||
// dismissButton = {
|
||||
// TextButton(onClick = {showDialog=false}) {
|
||||
// Text("Позже")
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = 15.dp)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Column {
|
||||
if (!isDomainsVerified) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(0.dp)
|
||||
.height(50.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = {
|
||||
val intent = Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
||||
.apply { data = Uri.fromParts("package", context.packageName, null) }
|
||||
context.startActivity(intent)
|
||||
}),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(255, 30, 30, 40))
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 10.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DomainVerification,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
)
|
||||
Text("Домены не подтвержены", fontSize = 18.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = search,
|
||||
onValueChange = {s -> search=s},
|
||||
leadingIcon = { Icon(imageVector = Icons.Default.Search, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Search
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
if (search.isNotBlank()) {
|
||||
navController.navigate("search/${URLEncoder.encode(search, "UTF-8")}")
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("source code", textDecoration = TextDecoration.Underline, modifier = Modifier.clickable(onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "https://codeberg.org/mi6e4ka/openstore".toUri())
|
||||
context.startActivity(intent)
|
||||
}))
|
||||
Text("v${BuildConfig.VERSION_NAME}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnResumeEffect(onResume: () -> Unit) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val currentOnResume by rememberUpdatedState(onResume)
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
currentOnResume()
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package dev.mi6e4ka.openstore.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package dev.mi6e4ka.openstore.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -9,50 +8,45 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
|||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
val md_theme_light_primary = Color(0xFF476810)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFC7F089)
|
||||
// ..
|
||||
// ..
|
||||
|
||||
val md_theme_dark_primary = Color(0xFFACD370)
|
||||
val md_theme_dark_onPrimary = Color(0xFF213600)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF324F00)
|
||||
// ..
|
||||
// ..
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
// ..
|
||||
)
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
// ..
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun OpenStoreTheme(
|
||||
fun Theme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
val colors = when {
|
||||
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
|
||||
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
MaterialTheme(colorScheme = colors, content = content)
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package dev.mi6e4ka.openstore.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
9
app/src/main/res/drawable/apk_install_24px.xml
Normal file
9
app/src/main/res/drawable/apk_install_24px.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,160Q80,127 103.5,103.5Q127,80 160,80L480,80L720,320L720,490L640,490L640,360L440,360L440,160L160,160Q160,160 160,160Q160,160 160,160L160,800Q160,800 160,800Q160,800 160,800L600,800L600,880L160,880ZM160,800L160,490L160,490L160,360L160,160L160,160Q160,160 160,160Q160,160 160,160L160,800Q160,800 160,800Q160,800 160,800ZM200,760Q204,711 230,670Q256,629 298,605L260,537Q260,536 264,522Q269,520 273.5,520Q278,520 280,525L319,595Q339,587 359,582.5Q379,578 400,578Q421,578 441,582.5Q461,587 481,595L520,525Q520,525 535,521Q540,523 541,528Q542,533 540,537L502,605Q544,629 570,670Q596,711 600,760L200,760ZM310,700Q318,700 324,694Q330,688 330,680Q330,672 324,666Q318,660 310,660Q302,660 296,666Q290,672 290,680Q290,688 296,694Q302,700 310,700ZM490,700Q498,700 504,694Q510,688 510,680Q510,672 504,666Q498,660 490,660Q482,660 476,666Q470,672 470,680Q470,688 476,694Q482,700 490,700ZM800,880L640,720L696,663L760,726L760,560L840,560L840,726L904,663L960,720L800,880Z"/>
|
||||
</vector>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.OpenStore" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.OpenStore" parent="android:Theme.DeviceDefault.NoActionBar" />
|
||||
</resources>
|
||||
5
app/src/main/res/xml/file_paths.xml
Normal file
5
app/src/main/res/xml/file_paths.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-cache-path name="cache" path="." />
|
||||
<external-files-path name="files" path="." />
|
||||
</paths>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package dev.mi6e4ka.openstore
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,4 +3,4 @@ plugins {
|
|||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,29 @@
|
|||
[versions]
|
||||
agp = "8.10.1"
|
||||
coilCompose = "2.2.2"
|
||||
converterGson = "2.9.0"
|
||||
agp = "8.12.1"
|
||||
coilCompose = "2.7.0"
|
||||
converterGson = "3.0.0"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.16.0"
|
||||
coreKtx = "1.17.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
kotlinxCoroutinesAndroid = "1.10.2"
|
||||
lifecycleRuntimeKtx = "2.9.1"
|
||||
lifecycleRuntimeKtx = "2.9.2"
|
||||
activityCompose = "1.10.1"
|
||||
composeBom = "2024.09.00"
|
||||
lifecycleViewmodelKtx = "2.6.1"
|
||||
navigationCompose = "2.9.0"
|
||||
retrofit = "2.9.0"
|
||||
composeBom = "2025.08.00"
|
||||
lifecycleViewmodelKtx = "2.9.2"
|
||||
navigationCompose = "2.9.3"
|
||||
retrofit = "3.0.0"
|
||||
loggingInterceptor = "5.1.0"
|
||||
loggingInterceptorVersion = "4.11.0"
|
||||
materialIconsExtendedAndroid = "1.7.8"
|
||||
accompanistPager = "0.36.0"
|
||||
accompanistPagerIndicators = "0.36.0"
|
||||
ackpineCore = "0.15.3"
|
||||
ackpineKtx = "0.15.2"
|
||||
runtimeLivedata = "1.9.0"
|
||||
material3 = "1.3.2"
|
||||
accompanistNavigationAnimation = "0.36.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
|
|
@ -31,12 +41,22 @@ androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
|||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.9.0" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||
navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "loggingInterceptor" }
|
||||
okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version = "4.12.0" }
|
||||
androidx-material-icons-extended-android = { group = "androidx.compose.material", name = "material-icons-extended-android", version.ref = "materialIconsExtendedAndroid" }
|
||||
accompanist-pager = { group = "com.google.accompanist", name = "accompanist-pager", version.ref = "accompanistPager" }
|
||||
accompanist-pager-indicators = { group = "com.google.accompanist", name = "accompanist-pager-indicators", version.ref = "accompanistPagerIndicators" }
|
||||
ackpine-core = { group = "ru.solrudev.ackpine", name = "ackpine-core", version.ref = "ackpineCore" }
|
||||
ackpine-ktx = { group = "ru.solrudev.ackpine", name = "ackpine-ktx", version.ref = "ackpineKtx" }
|
||||
androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
|
||||
material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
||||
accompanist-navigation-animation = { group = "com.google.accompanist", name = "accompanist-navigation-animation", version.ref = "accompanistNavigationAnimation" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
|||
#Tue Jun 24 22:14:23 MSK 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue