Merge branch 'compose-rewrite-audited' into 'dev'
compose: wrap up rewrite See merge request AuroraOSS/AuroraStore!567
This commit is contained in:
@@ -14,7 +14,6 @@ plugins {
|
|||||||
alias(libs.plugins.jetbrains.kotlin.parcelize)
|
alias(libs.plugins.jetbrains.kotlin.parcelize)
|
||||||
alias(libs.plugins.jetbrains.kotlin.serialization)
|
alias(libs.plugins.jetbrains.kotlin.serialization)
|
||||||
alias(libs.plugins.google.ksp)
|
alias(libs.plugins.google.ksp)
|
||||||
alias(libs.plugins.androidx.navigation)
|
|
||||||
alias(libs.plugins.ktlint)
|
alias(libs.plugins.ktlint)
|
||||||
alias(libs.plugins.rikka.tools.refine.plugin)
|
alias(libs.plugins.rikka.tools.refine.plugin)
|
||||||
alias(libs.plugins.hilt.android.plugin)
|
alias(libs.plugins.hilt.android.plugin)
|
||||||
@@ -128,23 +127,25 @@ configure<ApplicationExtension> {
|
|||||||
create("vanilla") {
|
create("vanilla") {
|
||||||
isDefault = true
|
isDefault = true
|
||||||
dimension = "device"
|
dimension = "device"
|
||||||
|
buildConfigField("Boolean", "SHOW_ANONYMOUS_LOGIN", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
create("huawei") {
|
create("huawei") {
|
||||||
dimension = "device"
|
dimension = "device"
|
||||||
versionNameSuffix = "-hw"
|
versionNameSuffix = "-hw"
|
||||||
|
buildConfigField("Boolean", "SHOW_ANONYMOUS_LOGIN", "false")
|
||||||
}
|
}
|
||||||
|
|
||||||
// This flavor is only for preloaded devices / users who push the app to system
|
// This flavor is only for preloaded devices / users who push the app to system
|
||||||
create("preload") {
|
create("preload") {
|
||||||
dimension = "device"
|
dimension = "device"
|
||||||
versionNameSuffix = "-preload"
|
versionNameSuffix = "-preload"
|
||||||
|
buildConfigField("Boolean", "SHOW_ANONYMOUS_LOGIN", "true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
viewBinding = true
|
|
||||||
aidl = true
|
aidl = true
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
@@ -193,8 +194,6 @@ dependencies {
|
|||||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
implementation(libs.androidx.lifecycle.navigation3)
|
implementation(libs.androidx.lifecycle.navigation3)
|
||||||
implementation(libs.androidx.preference.ktx)
|
implementation(libs.androidx.preference.ktx)
|
||||||
implementation(libs.androidx.swiperefreshlayout)
|
|
||||||
implementation(libs.androidx.viewpager2)
|
|
||||||
implementation(libs.androidx.work.runtime.ktx)
|
implementation(libs.androidx.work.runtime.ktx)
|
||||||
implementation(libs.androidx.paging.runtime)
|
implementation(libs.androidx.paging.runtime)
|
||||||
|
|
||||||
@@ -205,13 +204,12 @@ dependencies {
|
|||||||
implementation(libs.androidx.navigation3.runtime)
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
implementation(libs.androidx.navigation3.ui)
|
implementation(libs.androidx.navigation3.ui)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.androidx.navigation.fragment.ktx)
|
|
||||||
implementation(libs.androidx.navigation.ui.ktx)
|
|
||||||
|
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
|
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
|
implementation(libs.androidx.compose.runtime.livedata)
|
||||||
implementation(libs.androidx.ui)
|
implementation(libs.androidx.ui)
|
||||||
implementation(libs.androidx.ui.graphics)
|
implementation(libs.androidx.ui.graphics)
|
||||||
implementation(libs.androidx.ui.tooling.preview)
|
implementation(libs.androidx.ui.tooling.preview)
|
||||||
@@ -225,13 +223,6 @@ dependencies {
|
|||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.coil.network)
|
implementation(libs.coil.network)
|
||||||
|
|
||||||
// Shimmer
|
|
||||||
implementation(libs.facebook.shimmer)
|
|
||||||
|
|
||||||
// Epoxy
|
|
||||||
implementation(libs.airbnb.epoxy.android)
|
|
||||||
ksp(libs.airbnb.epoxy.processor)
|
|
||||||
|
|
||||||
// HTTP Clients
|
// HTTP Clients
|
||||||
implementation(libs.squareup.okhttp)
|
implementation(libs.squareup.okhttp)
|
||||||
|
|
||||||
|
|||||||
289
app/schemas/com.aurora.store.data.room.AuroraDatabase/7.json
Normal file
289
app/schemas/com.aurora.store.data.room.AuroraDatabase/7.json
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 7,
|
||||||
|
"identityHash": "2fad890b7c37341bc2b26ef023b76124",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "download",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `offerType` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `size` INTEGER NOT NULL, `id` INTEGER NOT NULL, `downloadStatus` TEXT NOT NULL, `progress` INTEGER NOT NULL, `speed` INTEGER NOT NULL, `timeRemaining` INTEGER NOT NULL, `totalFiles` INTEGER NOT NULL, `downloadedFiles` INTEGER NOT NULL, `fileList` TEXT NOT NULL, `sharedLibs` TEXT NOT NULL, `targetSdk` INTEGER NOT NULL, `downloadedAt` INTEGER NOT NULL, `requiresGMS` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "packageName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "versionCode",
|
||||||
|
"columnName": "versionCode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "offerType",
|
||||||
|
"columnName": "offerType",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isInstalled",
|
||||||
|
"columnName": "isInstalled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "iconURL",
|
||||||
|
"columnName": "iconURL",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "downloadStatus",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progress",
|
||||||
|
"columnName": "progress",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "speed",
|
||||||
|
"columnName": "speed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timeRemaining",
|
||||||
|
"columnName": "timeRemaining",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "totalFiles",
|
||||||
|
"columnName": "totalFiles",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadedFiles",
|
||||||
|
"columnName": "downloadedFiles",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fileList",
|
||||||
|
"columnName": "fileList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sharedLibs",
|
||||||
|
"columnName": "sharedLibs",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "targetSdk",
|
||||||
|
"columnName": "targetSdk",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadedAt",
|
||||||
|
"columnName": "downloadedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "requiresGMS",
|
||||||
|
"columnName": "requiresGMS",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"packageName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favourite",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `added` INTEGER NOT NULL, `mode` TEXT NOT NULL, PRIMARY KEY(`packageName`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "packageName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "iconURL",
|
||||||
|
"columnName": "iconURL",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "added",
|
||||||
|
"columnName": "added",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mode",
|
||||||
|
"columnName": "mode",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"packageName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "update",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `versionName` TEXT NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `changelog` TEXT NOT NULL, `id` INTEGER NOT NULL, `developerName` TEXT NOT NULL, `size` INTEGER NOT NULL, `updatedOn` TEXT NOT NULL, `hasValidCert` INTEGER NOT NULL, `offerType` INTEGER NOT NULL, `fileList` TEXT NOT NULL, `sharedLibs` TEXT NOT NULL, `targetSdk` INTEGER NOT NULL, `isIncompatible` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "packageName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "versionCode",
|
||||||
|
"columnName": "versionCode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "versionName",
|
||||||
|
"columnName": "versionName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "iconURL",
|
||||||
|
"columnName": "iconURL",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "changelog",
|
||||||
|
"columnName": "changelog",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "developerName",
|
||||||
|
"columnName": "developerName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updatedOn",
|
||||||
|
"columnName": "updatedOn",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hasValidCert",
|
||||||
|
"columnName": "hasValidCert",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "offerType",
|
||||||
|
"columnName": "offerType",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fileList",
|
||||||
|
"columnName": "fileList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sharedLibs",
|
||||||
|
"columnName": "sharedLibs",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "targetSdk",
|
||||||
|
"columnName": "targetSdk",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isIncompatible",
|
||||||
|
"columnName": "isIncompatible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"packageName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2fad890b7c37341bc2b26ef023b76124')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
312
app/schemas/com.aurora.store.data.room.AuroraDatabase/8.json
Normal file
312
app/schemas/com.aurora.store.data.room.AuroraDatabase/8.json
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 8,
|
||||||
|
"identityHash": "e21f5fa9464526f3ddfe0a7f58119d1a",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "download",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `offerType` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `size` INTEGER NOT NULL, `id` INTEGER NOT NULL, `downloadStatus` TEXT NOT NULL, `progress` INTEGER NOT NULL, `speed` INTEGER NOT NULL, `timeRemaining` INTEGER NOT NULL, `totalFiles` INTEGER NOT NULL, `downloadedFiles` INTEGER NOT NULL, `fileList` TEXT NOT NULL, `sharedLibs` TEXT NOT NULL, `targetSdk` INTEGER NOT NULL, `downloadedAt` INTEGER NOT NULL, `requiresGMS` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "packageName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "versionCode",
|
||||||
|
"columnName": "versionCode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "offerType",
|
||||||
|
"columnName": "offerType",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isInstalled",
|
||||||
|
"columnName": "isInstalled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "iconURL",
|
||||||
|
"columnName": "iconURL",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "downloadStatus",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progress",
|
||||||
|
"columnName": "progress",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "speed",
|
||||||
|
"columnName": "speed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timeRemaining",
|
||||||
|
"columnName": "timeRemaining",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "totalFiles",
|
||||||
|
"columnName": "totalFiles",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadedFiles",
|
||||||
|
"columnName": "downloadedFiles",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fileList",
|
||||||
|
"columnName": "fileList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sharedLibs",
|
||||||
|
"columnName": "sharedLibs",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "targetSdk",
|
||||||
|
"columnName": "targetSdk",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadedAt",
|
||||||
|
"columnName": "downloadedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "requiresGMS",
|
||||||
|
"columnName": "requiresGMS",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"packageName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favourite",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `added` INTEGER NOT NULL, `mode` TEXT NOT NULL, PRIMARY KEY(`packageName`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "packageName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "iconURL",
|
||||||
|
"columnName": "iconURL",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "added",
|
||||||
|
"columnName": "added",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mode",
|
||||||
|
"columnName": "mode",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"packageName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "update",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `versionName` TEXT NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `changelog` TEXT NOT NULL, `id` INTEGER NOT NULL, `developerName` TEXT NOT NULL, `size` INTEGER NOT NULL, `updatedOn` TEXT NOT NULL, `hasValidCert` INTEGER NOT NULL, `offerType` INTEGER NOT NULL, `fileList` TEXT NOT NULL, `sharedLibs` TEXT NOT NULL, `targetSdk` INTEGER NOT NULL, `isIncompatible` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "packageName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "versionCode",
|
||||||
|
"columnName": "versionCode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "versionName",
|
||||||
|
"columnName": "versionName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "iconURL",
|
||||||
|
"columnName": "iconURL",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "changelog",
|
||||||
|
"columnName": "changelog",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "developerName",
|
||||||
|
"columnName": "developerName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updatedOn",
|
||||||
|
"columnName": "updatedOn",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hasValidCert",
|
||||||
|
"columnName": "hasValidCert",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "offerType",
|
||||||
|
"columnName": "offerType",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fileList",
|
||||||
|
"columnName": "fileList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sharedLibs",
|
||||||
|
"columnName": "sharedLibs",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "targetSdk",
|
||||||
|
"columnName": "targetSdk",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isIncompatible",
|
||||||
|
"columnName": "isIncompatible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"packageName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ignored_update",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `ignoredVersionCode` INTEGER, PRIMARY KEY(`packageName`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "packageName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ignoredVersionCode",
|
||||||
|
"columnName": "ignoredVersionCode",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"packageName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e21f5fa9464526f3ddfe0a7f58119d1a')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.view.ui.splash
|
|
||||||
|
|
||||||
import com.aurora.extensions.hide
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class SplashFragment : BaseFlavouredSplashFragment() {
|
|
||||||
override fun attachActions() {
|
|
||||||
super.attachActions()
|
|
||||||
|
|
||||||
binding.btnAnonymous.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun resetActions() {
|
|
||||||
super.resetActions()
|
|
||||||
|
|
||||||
binding.btnAnonymous.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -82,9 +82,9 @@
|
|||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".ComposeActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@@ -103,16 +103,28 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<nav-graph android:value="@navigation/mobile_navigation" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="market" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<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:scheme="https" />
|
||||||
|
<data android:host="market.android.com" />
|
||||||
|
<data android:host="play.google.com" />
|
||||||
|
<data android:host="play.google.com" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Activity to host composable screens -->
|
|
||||||
<activity
|
|
||||||
android:name=".ComposeActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
|
|
||||||
<!-- Notification Action (Download Complete) -->
|
<!-- Notification Action (Download Complete) -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".data.activity.InstallActivity"
|
android:name=".data.activity.InstallActivity"
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ val isVAndAbove: Boolean
|
|||||||
val isMIUI: Boolean
|
val isMIUI: Boolean
|
||||||
get() = !getSystemProperty("ro.miui.ui.version.name").isNullOrBlank()
|
get() = !getSystemProperty("ro.miui.ui.version.name").isNullOrBlank()
|
||||||
|
|
||||||
|
val isHyperOS: Boolean
|
||||||
|
get() = !getSystemProperty("ro.mi.os.version.name").isNullOrBlank() ||
|
||||||
|
!getSystemProperty("ro.mi.os.version.code").isNullOrBlank()
|
||||||
|
|
||||||
|
val isGrapheneOS: Boolean
|
||||||
|
get() = Build.FINGERPRINT.contains("GrapheneOS", ignoreCase = true) ||
|
||||||
|
Build.HOST.contains("grapheneos", ignoreCase = true) ||
|
||||||
|
getSystemProperty("ro.build.flavor")?.contains("grapheneos", ignoreCase = true) == true
|
||||||
|
|
||||||
val isHuawei: Boolean
|
val isHuawei: Boolean
|
||||||
get() = Build.MANUFACTURER.lowercase(Locale.getDefault()).contains("huawei") ||
|
get() = Build.MANUFACTURER.lowercase(Locale.getDefault()).contains("huawei") ||
|
||||||
Build.HARDWARE.lowercase(Locale.getDefault()).contains("kirin") ||
|
Build.HARDWARE.lowercase(Locale.getDefault()).contains("kirin") ||
|
||||||
|
|||||||
@@ -6,43 +6,43 @@
|
|||||||
|
|
||||||
package com.aurora.store
|
package com.aurora.store
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.core.content.IntentCompat
|
import androidx.core.content.IntentCompat
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.aurora.extensions.getPackageName
|
import com.aurora.extensions.getPackageName
|
||||||
|
import com.aurora.store.compose.composition.LocalNetworkStatus
|
||||||
import com.aurora.store.compose.composition.LocalUI
|
import com.aurora.store.compose.composition.LocalUI
|
||||||
import com.aurora.store.compose.composition.UI
|
import com.aurora.store.compose.composition.UI
|
||||||
import com.aurora.store.compose.navigation.NavDisplay
|
import com.aurora.store.compose.navigation.NavDisplay
|
||||||
import com.aurora.store.compose.navigation.Screen
|
import com.aurora.store.compose.navigation.Screen
|
||||||
import com.aurora.store.compose.theme.AuroraTheme
|
import com.aurora.store.compose.theme.AuroraTheme
|
||||||
|
import com.aurora.store.data.model.NetworkStatus
|
||||||
|
import com.aurora.store.data.providers.NetworkProvider
|
||||||
|
import com.aurora.store.data.receiver.MigrationReceiver
|
||||||
import com.aurora.store.util.PackageUtil
|
import com.aurora.store.util.PackageUtil
|
||||||
|
import com.aurora.store.util.Preferences
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ComposeActivity : ComponentActivity() {
|
class ComposeActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
@Inject lateinit var networkProvider: NetworkProvider
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
MigrationReceiver.runMigrationsIfRequired(this)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
// Ensure the classloader is set for the Screen parcelable
|
|
||||||
intent.setExtrasClassLoader(Screen::class.java.classLoader)
|
intent.setExtrasClassLoader(Screen::class.java.classLoader)
|
||||||
|
|
||||||
var startDestination = IntentCompat.getParcelableExtra(
|
val startDestination = resolveStartDestination()
|
||||||
intent,
|
|
||||||
Screen.PARCEL_KEY,
|
|
||||||
Screen::class.java
|
|
||||||
) ?: Screen.Onboarding
|
|
||||||
|
|
||||||
// If the intent contains a package name for app details,
|
|
||||||
// Override the start destination to be the app details screen for that package.
|
|
||||||
val packageName = intent.getPackageName()
|
|
||||||
if (packageName != null) {
|
|
||||||
startDestination = Screen.AppDetails(packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
val localUI = when {
|
val localUI = when {
|
||||||
PackageUtil.isTv(this) -> UI.TV
|
PackageUtil.isTv(this) -> UI.TV
|
||||||
@@ -50,11 +50,45 @@ class ComposeActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
CompositionLocalProvider(LocalUI provides localUI) {
|
val networkStatus by networkProvider.status.collectAsStateWithLifecycle(
|
||||||
|
initialValue = NetworkStatus.AVAILABLE
|
||||||
|
)
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalUI provides localUI,
|
||||||
|
LocalNetworkStatus provides networkStatus
|
||||||
|
) {
|
||||||
AuroraTheme {
|
AuroraTheme {
|
||||||
NavDisplay(startDestination = startDestination)
|
NavDisplay(startDestination = startDestination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveStartDestination(): Screen {
|
||||||
|
// Parcel-based navigation (e.g. from NotificationUtil)
|
||||||
|
IntentCompat.getParcelableExtra(intent, Screen.PARCEL_KEY, Screen::class.java)
|
||||||
|
?.let { return it }
|
||||||
|
|
||||||
|
// Deep links via ACTION_VIEW
|
||||||
|
if (intent.action == Intent.ACTION_VIEW) {
|
||||||
|
val data = intent.data
|
||||||
|
val path = data?.path.orEmpty()
|
||||||
|
val id = data?.getQueryParameter("id")
|
||||||
|
return when {
|
||||||
|
id != null && path.contains("/apps/dev") -> Screen.DevProfile(id)
|
||||||
|
id != null -> Screen.AppDetails(id)
|
||||||
|
else -> defaultStart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEND / SHOW_APP_INFO — getPackageName() handles both
|
||||||
|
intent.getPackageName()?.let { return Screen.AppDetails(it) }
|
||||||
|
|
||||||
|
return defaultStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultStart(): Screen = when {
|
||||||
|
!Preferences.getBoolean(this, Preferences.PREFERENCE_INTRO) -> Screen.Onboarding
|
||||||
|
else -> Screen.Splash
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
* Copyright (C) 2022, The Calyx Institute
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.addCallback
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat.Type.displayCutout
|
|
||||||
import androidx.core.view.WindowInsetsCompat.Type.ime
|
|
||||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.FloatingWindow
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import androidx.navigation.ui.setupWithNavController
|
|
||||||
import com.aurora.store.data.model.NetworkStatus
|
|
||||||
import com.aurora.store.data.receiver.MigrationReceiver
|
|
||||||
import com.aurora.store.databinding.ActivityMainBinding
|
|
||||||
import com.aurora.store.util.PackageUtil
|
|
||||||
import com.aurora.store.util.Preferences
|
|
||||||
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
|
|
||||||
import com.aurora.store.view.ui.sheets.NetworkDialogSheet
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private val viewModel: MainViewModel by viewModels()
|
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
|
||||||
|
|
||||||
// TopLevelFragments
|
|
||||||
private val topLevelFrags = listOf(
|
|
||||||
R.id.appsContainerFragment,
|
|
||||||
R.id.gamesContainerFragment,
|
|
||||||
R.id.updatesFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
// Check and run migrations first if required
|
|
||||||
// This is needed thanks to OEMs breaking the MY_PACKAGE_REPLACED API
|
|
||||||
MigrationReceiver.runMigrationsIfRequired(this)
|
|
||||||
|
|
||||||
enableEdgeToEdge()
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
// Adjust root view's paddings for edgeToEdge display
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { root, windowInsets ->
|
|
||||||
val insets = windowInsets.getInsets(systemBars() or displayCutout() or ime())
|
|
||||||
root.setPadding(insets.left, insets.top, insets.right, 0)
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
|
|
||||||
val navHostFragment =
|
|
||||||
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
|
||||||
val navController = navHostFragment.navController
|
|
||||||
|
|
||||||
if (!PackageUtil.isTv(this)) {
|
|
||||||
viewModel.networkProvider.status.onEach { networkStatus ->
|
|
||||||
when (networkStatus) {
|
|
||||||
NetworkStatus.AVAILABLE -> {
|
|
||||||
if (!supportFragmentManager.isDestroyed && isIntroDone()) {
|
|
||||||
val fragment = supportFragmentManager
|
|
||||||
.findFragmentByTag(NetworkDialogSheet.TAG)
|
|
||||||
fragment?.let {
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.remove(fragment)
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkStatus.UNAVAILABLE -> {
|
|
||||||
if (!supportFragmentManager.isDestroyed && isIntroDone()) {
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.add(NetworkDialogSheet.newInstance(), NetworkDialogSheet.TAG)
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.launchIn(AuroraApp.scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.navView.setupWithNavController(navController)
|
|
||||||
|
|
||||||
// Handle quick exit from back actions
|
|
||||||
val defaultTab = when (Preferences.getInteger(this, PREFERENCE_DEFAULT_SELECTED_TAB)) {
|
|
||||||
1 -> R.id.gamesContainerFragment
|
|
||||||
2 -> R.id.updatesFragment
|
|
||||||
else -> R.id.appsContainerFragment
|
|
||||||
}
|
|
||||||
onBackPressedDispatcher.addCallback(this) {
|
|
||||||
if (navController.currentDestination?.id in topLevelFrags) {
|
|
||||||
if (navController.currentDestination?.id == defaultTab) {
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
navController.navigate(defaultTab)
|
|
||||||
}
|
|
||||||
} else if (navHostFragment.childFragmentManager.backStackEntryCount == 0) {
|
|
||||||
// We are on either on onboarding or splash fragment
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
navController.navigateUp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle views on fragments
|
|
||||||
navController.addOnDestinationChangedListener { _, navDestination, _ ->
|
|
||||||
if (navDestination !is FloatingWindow) {
|
|
||||||
when (navDestination.id) {
|
|
||||||
in topLevelFrags -> binding.navView.visibility = View.VISIBLE
|
|
||||||
else -> binding.navView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates
|
|
||||||
lifecycleScope.launch {
|
|
||||||
viewModel.updateHelper.updates.collectLatest { list ->
|
|
||||||
binding.navView.getOrCreateBadge(R.id.updatesFragment).apply {
|
|
||||||
isVisible = !list.isNullOrEmpty()
|
|
||||||
number = list?.size ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isIntroDone(): Boolean = Preferences.getBoolean(this, Preferences.PREFERENCE_INTRO)
|
|
||||||
}
|
|
||||||
@@ -7,12 +7,10 @@ package com.aurora.store
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.aurora.store.data.helper.UpdateHelper
|
import com.aurora.store.data.helper.UpdateHelper
|
||||||
import com.aurora.store.data.providers.NetworkProvider
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel @Inject constructor(
|
class MainViewModel @Inject constructor(
|
||||||
val networkProvider: NetworkProvider,
|
|
||||||
val updateHelper: UpdateHelper
|
val updateHelper: UpdateHelper
|
||||||
) : ViewModel()
|
) : ViewModel()
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single shared layout for every list-row in the app. A leading slot (icon, image…), a
|
||||||
|
* stacked headline + supporting + tertiary text column, and a trailing slot (checkbox,
|
||||||
|
* button, chip…). [headlineStyle] toggles between data-row (bodyMedium) and settings-row
|
||||||
|
* (bodyLarge) typography while keeping every other dimension consistent.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AuroraListItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
headline: String,
|
||||||
|
supporting: String? = null,
|
||||||
|
tertiary: String? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
leading: (@Composable () -> Unit)? = null,
|
||||||
|
trailing: (@Composable () -> Unit)? = null,
|
||||||
|
headlineStyle: TextStyle = MaterialTheme.typography.bodyLarge
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.then(
|
||||||
|
if (onClick != null) {
|
||||||
|
Modifier.clickable(enabled = enabled, onClick = onClick)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (leading != null) leading()
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = headline,
|
||||||
|
style = headlineStyle,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
if (!supporting.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = supporting,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!tertiary.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = tertiary,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (trailing != null) trailing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AuroraListItemPreview() {
|
||||||
|
AuroraListItem(
|
||||||
|
headline = "Example app",
|
||||||
|
supporting = "com.example.app",
|
||||||
|
tertiary = "v1.0.0 (100)"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,23 +8,16 @@ package com.aurora.store.compose.composable
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
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.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.ColorMatrix
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
@@ -33,18 +26,6 @@ import com.aurora.store.BuildConfig
|
|||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable for displaying package details in a list for blacklisting
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param icon Icon for the package
|
|
||||||
* @param displayName User-readable name of the package
|
|
||||||
* @param packageName Name of the package
|
|
||||||
* @param versionName versionName of the package
|
|
||||||
* @param versionCode versionCode of the package
|
|
||||||
* @param isChecked Whether the app is blacklisted
|
|
||||||
* @param isEnabled Whether this app is allowed to be blacklisted
|
|
||||||
* @param onClick Callback when the composable is clicked
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BlackListItem(
|
fun BlackListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -57,50 +38,30 @@ fun BlackListItem(
|
|||||||
isEnabled: Boolean = true,
|
isEnabled: Boolean = true,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
AuroraListItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
headline = displayName,
|
||||||
.clickable(enabled = isEnabled, onClick = onClick)
|
supporting = packageName,
|
||||||
.padding(
|
tertiary = stringResource(R.string.version, versionName, versionCode),
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
headlineStyle = MaterialTheme.typography.bodyMedium,
|
||||||
vertical = dimensionResource(R.dimen.padding_xsmall)
|
onClick = onClick,
|
||||||
),
|
enabled = isEnabled,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
leading = {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Row(modifier = Modifier.weight(1F)) {
|
|
||||||
Image(
|
Image(
|
||||||
bitmap = icon.asImageBitmap(),
|
bitmap = icon.asImageBitmap(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
|
||||||
|
colorFilter = if (isChecked) DESATURATE else null
|
||||||
)
|
)
|
||||||
Column(
|
},
|
||||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
trailing = {
|
||||||
) {
|
Checkbox(checked = isChecked, enabled = isEnabled, onCheckedChange = { onClick() })
|
||||||
Text(
|
|
||||||
text = displayName,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = packageName,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.version, versionName, versionCode),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Checkbox(checked = isChecked, enabled = isEnabled, onCheckedChange = { onClick() })
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val DESATURATE = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) })
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import com.aurora.gplayapi.data.models.Category
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CategoryItem(modifier: Modifier = Modifier, category: Category, onClick: () -> Unit = {}) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_small)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_large))
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(category.imageUrl)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredSize(dimensionResource(R.dimen.icon_size_category))
|
||||||
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = category.title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun CategoryItemPreview() {
|
||||||
|
CategoryItem(
|
||||||
|
category = Category(
|
||||||
|
title = "Entertainment",
|
||||||
|
imageUrl = ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ fun ContainedLoadingIndicator(modifier: Modifier = Modifier) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(dimensionResource(R.dimen.padding_small)),
|
.padding(dimensionResource(R.dimen.spacing_small)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
val description = stringResource(R.string.loading)
|
val description = stringResource(R.string.loading)
|
||||||
|
|||||||
@@ -5,34 +5,13 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
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.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.dimensionResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display device details for spoofing in a list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param userReadableName Name of the device, obtained through `UserReadableName` property
|
|
||||||
* @param manufacturer Name of the device manufacturer, obtained through `Build.MANUFACTURER` property
|
|
||||||
* @param androidVersionSdk Android version on the device, obtained through `Build.VERSION.SDK_INT` property
|
|
||||||
* @param platforms Platforms supported on the device, obtained through `Platforms` property
|
|
||||||
* @param isChecked If the device is selected
|
|
||||||
* @param onClick Callback when the composable is clicked
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceListItem(
|
fun DeviceListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -43,34 +22,16 @@ fun DeviceListItem(
|
|||||||
isChecked: Boolean = false,
|
isChecked: Boolean = false,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
AuroraListItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
headline = userReadableName,
|
||||||
.clickable(onClick = { if (!isChecked) onClick() })
|
supporting = stringResource(R.string.spoof_property, manufacturer, androidVersionSdk),
|
||||||
.padding(dimensionResource(R.dimen.padding_small)),
|
tertiary = platforms.replace(",\\s*".toRegex(), ", "),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
onClick = { if (!isChecked) onClick() },
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
trailing = {
|
||||||
) {
|
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
||||||
Column(modifier = Modifier.weight(1F)) {
|
|
||||||
Text(
|
|
||||||
text = userReadableName,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.spoof_property, manufacturer, androidVersionSdk),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = platforms.replace(",\\s*".toRegex(), ", "),
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
|
|||||||
@@ -5,32 +5,17 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.dimensionResource
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display dispenser URL in a list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param url URL of the dispenser
|
|
||||||
* @param onClick Callback when this URL is clicked
|
|
||||||
* @param onClear Callback when the clear button is clicked
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DispenserListItem(
|
fun DispenserListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -38,37 +23,23 @@ fun DispenserListItem(
|
|||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onClear: () -> Unit = {}
|
onClear: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
AuroraListItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
headline = url,
|
||||||
.clickable(onClick = onClick)
|
onClick = onClick,
|
||||||
.padding(dimensionResource(R.dimen.padding_small)),
|
leading = {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Icon(painter = painterResource(R.drawable.ic_server), contentDescription = null)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
},
|
||||||
) {
|
trailing = {
|
||||||
Row(
|
TextButton(onClick = onClear) {
|
||||||
modifier = Modifier.weight(1F),
|
Text(
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
text = stringResource(R.string.remove),
|
||||||
) {
|
maxLines = 1,
|
||||||
Icon(
|
overflow = TextOverflow.Ellipsis
|
||||||
painter = painterResource(R.drawable.ic_server),
|
)
|
||||||
contentDescription = null
|
}
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = url,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
TextButton(onClick = onClear) {
|
)
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.remove),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
|
|||||||
@@ -8,36 +8,15 @@ package com.aurora.store.compose.composable
|
|||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import androidx.compose.animation.AnimatedContent
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
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.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
@@ -46,141 +25,73 @@ import com.aurora.store.R
|
|||||||
import com.aurora.store.compose.composable.app.AnimatedAppIcon
|
import com.aurora.store.compose.composable.app.AnimatedAppIcon
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import com.aurora.store.compose.theme.colorGreen
|
||||||
|
import com.aurora.store.compose.theme.colorRed
|
||||||
import com.aurora.store.data.model.DownloadStatus
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
import com.aurora.store.data.room.download.Download
|
import com.aurora.store.data.room.download.Download
|
||||||
import com.aurora.store.util.CommonUtil.getETAString
|
import com.aurora.store.util.CommonUtil.getETAString
|
||||||
import com.aurora.store.util.PackageUtil
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display details of a download in a list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param download [Download] to display
|
|
||||||
* @param onClick Callback when this composable is clicked
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadListItem(
|
fun DownloadListItem(modifier: Modifier = Modifier, download: Download, onClick: () -> Unit = {}) {
|
||||||
modifier: Modifier = Modifier,
|
val context = LocalContext.current
|
||||||
download: Download,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
onClear: () -> Unit = {},
|
|
||||||
onCancel: () -> Unit = {},
|
|
||||||
onExport: () -> Unit = {},
|
|
||||||
onInstall: () -> Unit = {},
|
|
||||||
isExpanded: Boolean = false
|
|
||||||
) {
|
|
||||||
val progress = "${download.progress}%"
|
|
||||||
val speed = "${Formatter.formatShortFileSize(LocalContext.current, download.speed)}/s"
|
|
||||||
val eta = getETAString(LocalContext.current, download.timeRemaining)
|
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val statusLine = stringResource(download.status.localized)
|
||||||
var isVisible by remember { mutableStateOf(true) }
|
val tertiaryLine = if (download.status in DownloadStatus.running) {
|
||||||
var isExpanded by remember { mutableStateOf(isExpanded) }
|
val progress = "${download.progress}%"
|
||||||
|
val speed = "${Formatter.formatShortFileSize(context, download.speed)}/s"
|
||||||
fun requestClear() {
|
val eta = getETAString(context, download.timeRemaining)
|
||||||
coroutineScope.launch {
|
"$progress • $speed • $eta"
|
||||||
isVisible = false
|
} else {
|
||||||
delay(300) // Let the animation play
|
DateUtils.formatDateTime(context, download.downloadedAt, DateUtils.FORMAT_SHOW_DATE)
|
||||||
onClear()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
AuroraListItem(
|
||||||
fun ExpandedMenu() {
|
modifier = modifier,
|
||||||
val context = LocalContext.current
|
headline = download.displayName,
|
||||||
Box(modifier = modifier) {
|
supporting = statusLine,
|
||||||
IconButton(onClick = { isExpanded = true }) {
|
tertiary = tertiaryLine,
|
||||||
Icon(
|
headlineStyle = MaterialTheme.typography.bodyMedium,
|
||||||
painter = painterResource(R.drawable.ic_more_vert),
|
onClick = onClick,
|
||||||
contentDescription = stringResource(R.string.menu)
|
leading = {
|
||||||
)
|
AnimatedAppIcon(
|
||||||
}
|
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
|
||||||
DropdownMenu(expanded = isExpanded, onDismissRequest = { isExpanded = false }) {
|
iconUrl = download.iconURL,
|
||||||
DropdownMenuItem(
|
inProgress = download.isRunning,
|
||||||
text = { Text(text = stringResource(R.string.action_cancel)) },
|
progress = download.progress.toFloat()
|
||||||
onClick = onCancel,
|
)
|
||||||
enabled = download.isRunning
|
},
|
||||||
)
|
trailing = when (download.status) {
|
||||||
DropdownMenuItem(
|
DownloadStatus.COMPLETED -> {
|
||||||
text = { Text(text = stringResource(R.string.action_install)) },
|
{
|
||||||
onClick = onInstall,
|
Icon(
|
||||||
enabled = download.canInstall(context)
|
painter = painterResource(R.drawable.ic_check),
|
||||||
)
|
contentDescription = stringResource(R.string.download_completed),
|
||||||
DropdownMenuItem(
|
tint = colorGreen
|
||||||
text = { Text(text = stringResource(R.string.action_export)) },
|
|
||||||
onClick = onExport,
|
|
||||||
enabled = download.canInstall(context) ||
|
|
||||||
PackageUtil.isInstalled(
|
|
||||||
context,
|
|
||||||
download.packageName
|
|
||||||
)
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.action_clear)) },
|
|
||||||
onClick = ::requestClear,
|
|
||||||
enabled = !download.isRunning
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(visible = isVisible, exit = shrinkVertically() + fadeOut()) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(
|
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
|
||||||
vertical = dimensionResource(R.dimen.padding_small)
|
|
||||||
),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Row(modifier = Modifier.weight(1F)) {
|
|
||||||
AnimatedAppIcon(
|
|
||||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
|
|
||||||
iconUrl = download.iconURL,
|
|
||||||
inProgress = download.isRunning,
|
|
||||||
progress = download.progress.toFloat()
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = download.displayName,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
Text(
|
|
||||||
text = stringResource(download.status.localized),
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
AnimatedContent(targetState = download.status) { status ->
|
|
||||||
Text(
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
text = if (status in DownloadStatus.running) {
|
|
||||||
"$progress • $speed • $eta"
|
|
||||||
} else {
|
|
||||||
DateUtils.formatDateTime(
|
|
||||||
LocalContext.current,
|
|
||||||
download.downloadedAt,
|
|
||||||
DateUtils.FORMAT_SHOW_DATE
|
|
||||||
).toString()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ExpandedMenu()
|
|
||||||
|
DownloadStatus.CANCELLED, DownloadStatus.FAILED
|
||||||
|
-> {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_cancel),
|
||||||
|
contentDescription = stringResource(R.string.download_canceled),
|
||||||
|
tint = colorRed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DownloadListItemPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
private fun DownloadListItemPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
DownloadListItem(download = Download.fromApp(app), isExpanded = true)
|
DownloadListItem(download = Download.fromApp(app))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,28 +6,12 @@
|
|||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
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.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
@@ -35,7 +19,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
@@ -47,16 +30,7 @@ import com.aurora.store.R
|
|||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.data.room.favourite.Favourite
|
import com.aurora.store.data.room.favourite.Favourite
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display a favourite app in a list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param favourite A [Favourite] app to display
|
|
||||||
* @param onClick Callback when this composable is clicked
|
|
||||||
* @param onClear Callback when the favourite button is clicked to remove the app from favourites
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FavouriteListItem(
|
fun FavouriteListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -64,35 +38,22 @@ fun FavouriteListItem(
|
|||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onClear: () -> Unit = {}
|
onClear: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val context = LocalContext.current
|
||||||
var isVisible by remember { mutableStateOf(true) }
|
RemovableListItem(onRemove = onClear) { triggerRemove ->
|
||||||
|
AuroraListItem(
|
||||||
fun requestClear() {
|
modifier = modifier,
|
||||||
coroutineScope.launch {
|
headline = favourite.displayName,
|
||||||
isVisible = false
|
supporting = favourite.packageName,
|
||||||
delay(300) // Let the animation play
|
tertiary = DateUtils.formatDateTime(
|
||||||
onClear()
|
context,
|
||||||
}
|
favourite.added,
|
||||||
}
|
DateUtils.FORMAT_SHOW_DATE
|
||||||
|
),
|
||||||
AnimatedVisibility(
|
headlineStyle = MaterialTheme.typography.bodyMedium,
|
||||||
visible = isVisible,
|
onClick = onClick,
|
||||||
exit = shrinkVertically() + fadeOut()
|
leading = {
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(
|
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
|
||||||
vertical = dimensionResource(R.dimen.padding_xsmall)
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Row(modifier = Modifier.weight(1F)) {
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(context)
|
||||||
.data(favourite.iconURL)
|
.data(favourite.iconURL)
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.build(),
|
.build(),
|
||||||
@@ -102,40 +63,16 @@ fun FavouriteListItem(
|
|||||||
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
||||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
||||||
)
|
)
|
||||||
Column(
|
},
|
||||||
modifier = Modifier.padding(
|
trailing = {
|
||||||
horizontal = dimensionResource(R.dimen.margin_small)
|
IconButton(onClick = triggerRemove) {
|
||||||
)
|
Icon(
|
||||||
) {
|
painter = painterResource(R.drawable.ic_favorite_checked),
|
||||||
Text(
|
contentDescription = stringResource(R.string.action_favourite)
|
||||||
text = favourite.displayName,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = favourite.packageName,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = DateUtils.formatDateTime(
|
|
||||||
LocalContext.current,
|
|
||||||
favourite.added,
|
|
||||||
DateUtils.FORMAT_SHOW_DATE
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(onClick = { requestClear() }) {
|
)
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_favorite_checked),
|
|
||||||
contentDescription = stringResource(R.string.action_favourite)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,10 +57,10 @@ fun Info(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null)
|
.clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null)
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
vertical = dimensionResource(R.dimen.padding_xxsmall)
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
),
|
),
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal)),
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small)),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (painter != null) Icon(painter = painter, contentDescription = null)
|
if (painter != null) Icon(painter = painter, contentDescription = null)
|
||||||
|
|||||||
@@ -5,35 +5,16 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
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.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.dimensionResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.data.installer.SessionInstaller
|
import com.aurora.store.data.installer.SessionInstaller
|
||||||
import com.aurora.store.data.model.InstallerInfo
|
import com.aurora.store.data.model.InstallerInfo
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display installer details in a list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param installerInfo A [InstallerInfo] object to display details
|
|
||||||
* @param isSelected Whether this installer is selected
|
|
||||||
* @param onClick Callback when this composable is clicked
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InstallerListItem(
|
fun InstallerListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -41,34 +22,14 @@ fun InstallerListItem(
|
|||||||
isSelected: Boolean = false,
|
isSelected: Boolean = false,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
AuroraListItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
headline = stringResource(installerInfo.title),
|
||||||
.clickable(onClick = onClick)
|
supporting = stringResource(installerInfo.subtitle),
|
||||||
.padding(dimensionResource(R.dimen.padding_small)),
|
tertiary = stringResource(installerInfo.description),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
onClick = onClick,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
trailing = { RadioButton(selected = isSelected, onClick = onClick) }
|
||||||
) {
|
)
|
||||||
Column(modifier = Modifier.weight(1F)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(installerInfo.title),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(installerInfo.subtitle),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(installerInfo.description),
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RadioButton(selected = isSelected, onClick = onClick)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
|||||||
@@ -6,21 +6,9 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
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.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.VerticalDivider
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -29,7 +17,6 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
@@ -38,12 +25,6 @@ import com.aurora.store.R
|
|||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.data.model.Link
|
import com.aurora.store.data.model.Link
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to show link details in a list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param link [Link] to show details
|
|
||||||
* @param onClick Callback when the composable is clicked
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LinkListItem(
|
fun LinkListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -51,46 +32,25 @@ fun LinkListItem(
|
|||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
iconTint: Color? = null
|
iconTint: Color? = null
|
||||||
) {
|
) {
|
||||||
Row(
|
AuroraListItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
headline = link.title,
|
||||||
.clickable(onClick = onClick)
|
supporting = link.subtitle,
|
||||||
.padding(dimensionResource(R.dimen.padding_small)),
|
onClick = onClick,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
leading = {
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
AsyncImage(
|
||||||
) {
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
AsyncImage(
|
.data(link.icon)
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
.build(),
|
||||||
.data(link.icon)
|
contentDescription = null,
|
||||||
.build(),
|
contentScale = ContentScale.Crop,
|
||||||
contentDescription = null,
|
modifier = Modifier
|
||||||
contentScale = ContentScale.Crop,
|
.requiredSize(dimensionResource(R.dimen.icon_size_default))
|
||||||
modifier = Modifier
|
.clip(CircleShape),
|
||||||
.requiredSize(dimensionResource(R.dimen.icon_size_default))
|
colorFilter = if (iconTint != null) ColorFilter.tint(color = iconTint) else null
|
||||||
.clip(CircleShape),
|
|
||||||
colorFilter = if (iconTint != null) ColorFilter.tint(color = iconTint) else null
|
|
||||||
)
|
|
||||||
VerticalDivider(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = dimensionResource(R.dimen.margin_large))
|
|
||||||
.width(dimensionResource(R.dimen.padding_xxsmall))
|
|
||||||
.height(dimensionResource(R.dimen.height_header))
|
|
||||||
)
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = link.title,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = link.subtitle,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
|||||||
@@ -5,32 +5,12 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
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.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.dimensionResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.aurora.store.R
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display locale details in a list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param displayName Display name of the locale
|
|
||||||
* @param displayLanguage Display name of the language in the locale
|
|
||||||
* @param isChecked Whether the locale is checked/selected
|
|
||||||
* @param onClick Callback when the composable is clicked
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LocaleListItem(
|
fun LocaleListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -39,30 +19,15 @@ fun LocaleListItem(
|
|||||||
isChecked: Boolean = false,
|
isChecked: Boolean = false,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
AuroraListItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
headline = displayName,
|
||||||
.clickable(onClick = { if (!isChecked) onClick() })
|
supporting = displayLanguage,
|
||||||
.padding(dimensionResource(R.dimen.padding_small)),
|
onClick = { if (!isChecked) onClick() },
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
trailing = {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1F)) {
|
|
||||||
Text(
|
|
||||||
text = displayName,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = displayLanguage,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ fun Logo(modifier: Modifier = Modifier) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.margin_small),
|
dimensionResource(R.dimen.spacing_small),
|
||||||
Alignment.CenterVertically
|
Alignment.CenterVertically
|
||||||
),
|
),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
|||||||
@@ -91,13 +91,13 @@ fun MicroG(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = dimensionResource(R.dimen.padding_small),
|
horizontal = dimensionResource(R.dimen.spacing_small),
|
||||||
vertical = dimensionResource(R.dimen.padding_xxsmall)
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.onboarding_gms_microg),
|
text = stringResource(R.string.onboarding_gms_microg),
|
||||||
@@ -116,8 +116,8 @@ fun MicroG(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_large)),
|
.padding(vertical = dimensionResource(R.dimen.spacing_large)),
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
) {
|
) {
|
||||||
uiState.items.fastForEach { item ->
|
uiState.items.fastForEach { item ->
|
||||||
MicroGItemRow(item = item)
|
MicroGItemRow(item = item)
|
||||||
@@ -127,7 +127,7 @@ fun MicroG(
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.padding_small)
|
dimensionResource(R.dimen.spacing_small)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
@@ -218,8 +218,8 @@ private fun MicroGItemRow(item: ExternalItem) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = dimensionResource(R.dimen.padding_small),
|
horizontal = dimensionResource(R.dimen.spacing_small),
|
||||||
vertical = dimensionResource(R.dimen.padding_xsmall)
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -232,7 +232,7 @@ private fun MicroGItemRow(item: ExternalItem) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1F)
|
.weight(1F)
|
||||||
.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
.padding(horizontal = dimensionResource(R.dimen.spacing_small))
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = item.displayName,
|
text = item.displayName,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I
|
|||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.margin_xsmall),
|
dimensionResource(R.dimen.spacing_xsmall),
|
||||||
Alignment.CenterHorizontally
|
Alignment.CenterHorizontally
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -60,7 +60,7 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I
|
|||||||
)
|
)
|
||||||
val size by animateDpAsState(
|
val size by animateDpAsState(
|
||||||
targetValue = if (isSelected) {
|
targetValue = if (isSelected) {
|
||||||
dimensionResource(R.dimen.radius_normal)
|
dimensionResource(R.dimen.radius_medium)
|
||||||
} else {
|
} else {
|
||||||
dimensionResource(R.dimen.radius_small)
|
dimensionResource(R.dimen.radius_small)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ fun PermissionList(
|
|||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||||
) {
|
) {
|
||||||
if (header != null) stickyHeader(content = header)
|
if (header != null) stickyHeader(content = header)
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,10 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.dimensionResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -26,49 +18,30 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
import com.aurora.store.data.model.Permission
|
import com.aurora.store.data.model.Permission
|
||||||
import com.aurora.store.data.model.PermissionType
|
import com.aurora.store.data.model.PermissionType
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display permission details in a list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param permission [Permission] to display
|
|
||||||
* @param onAction Callback when the user clicks the action button
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PermissionListItem(
|
fun PermissionListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
permission: Permission,
|
permission: Permission,
|
||||||
onAction: () -> Unit = {}
|
onAction: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
AuroraListItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
headline = permission.title,
|
||||||
.padding(dimensionResource(R.dimen.padding_small)),
|
supporting = permission.subtitle,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
trailing = {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
TextButton(onClick = onAction, enabled = !permission.isGranted) {
|
||||||
) {
|
Text(
|
||||||
Column(modifier = Modifier.weight(1F)) {
|
text = if (permission.isGranted) {
|
||||||
Text(
|
stringResource(R.string.action_granted)
|
||||||
text = permission.title,
|
} else {
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
stringResource(R.string.action_grant)
|
||||||
maxLines = 1,
|
},
|
||||||
overflow = TextOverflow.Ellipsis
|
maxLines = 1,
|
||||||
)
|
overflow = TextOverflow.Ellipsis
|
||||||
Text(
|
)
|
||||||
text = permission.subtitle,
|
}
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
TextButton(onClick = onAction, enabled = !permission.isGranted) {
|
)
|
||||||
Text(
|
|
||||||
text = if (permission.isGranted) {
|
|
||||||
stringResource(R.string.action_granted)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.action_grant)
|
|
||||||
},
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
@@ -8,11 +9,11 @@ package com.aurora.store.compose.composable
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -29,27 +30,23 @@ import com.aurora.store.R
|
|||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable to show error message that fills all available screen
|
* Full-screen empty/error placeholder: centered icon + message and an optional action button.
|
||||||
* @param modifier The modifier to be applied to the composable
|
* Replaces the previous EmptyState and Error composables.
|
||||||
* @param painter Painter to draw the icon
|
|
||||||
* @param message Message for error
|
|
||||||
* @param actionMessage Message to show on action button; defaults to null with button not visible
|
|
||||||
* @param onAction Callback when action button is clicked
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Error(
|
fun Placeholder(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
painter: Painter,
|
painter: Painter,
|
||||||
message: String,
|
message: String,
|
||||||
actionMessage: String? = null,
|
actionLabel: String? = null,
|
||||||
onAction: (() -> Unit)? = null
|
onAction: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.margin_small),
|
dimensionResource(R.dimen.spacing_small),
|
||||||
Alignment.CenterVertically
|
Alignment.CenterVertically
|
||||||
),
|
),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
@@ -57,18 +54,18 @@ fun Error(
|
|||||||
Icon(
|
Icon(
|
||||||
painter = painter,
|
painter = painter,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size))
|
modifier = Modifier.size(dimensionResource(R.dimen.icon_size_medium)),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = message,
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
if (actionLabel != null && onAction != null) {
|
||||||
if (actionMessage != null) {
|
FilledTonalButton(onClick = onAction) {
|
||||||
Button(onClick = { if (onAction != null) onAction() }, enabled = onAction != null) {
|
|
||||||
Text(
|
Text(
|
||||||
text = actionMessage,
|
text = actionLabel,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
@@ -80,10 +77,11 @@ fun Error(
|
|||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ErrorPreview() {
|
private fun PlaceholderPreview() {
|
||||||
Error(
|
Placeholder(
|
||||||
painter = painterResource(R.drawable.ic_updates),
|
painter = painterResource(R.drawable.ic_updates),
|
||||||
message = stringResource(R.string.details_no_updates),
|
message = stringResource(R.string.details_no_updates),
|
||||||
actionMessage = stringResource(R.string.check_updates)
|
actionLabel = stringResource(R.string.check_updates),
|
||||||
|
onAction = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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 kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val REMOVE_ANIM_DURATION_MS = 300L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps [content] in an AnimatedVisibility container that shrinks vertically and fades out
|
||||||
|
* before invoking [onRemove]. The content lambda receives a trigger callback that, when
|
||||||
|
* invoked, plays the exit animation and then calls [onRemove].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RemovableListItem(
|
||||||
|
onRemove: () -> Unit,
|
||||||
|
content: @Composable (triggerRemove: () -> Unit) -> Unit
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var visible by remember { mutableStateOf(true) }
|
||||||
|
AnimatedVisibility(visible = visible, exit = shrinkVertically() + fadeOut()) {
|
||||||
|
content {
|
||||||
|
scope.launch {
|
||||||
|
visible = false
|
||||||
|
delay(REMOVE_ANIM_DURATION_MS)
|
||||||
|
onRemove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,8 +36,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment.Companion.Center
|
import androidx.compose.ui.Alignment.Companion.Center
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
@@ -48,7 +48,6 @@ import kotlinx.coroutines.launch
|
|||||||
fun ScrollHint(
|
fun ScrollHint(
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
bottomPadding: Dp = 72.dp, // keep above bottom bar
|
|
||||||
autoHideOnScroll: Boolean = true,
|
autoHideOnScroll: Boolean = true,
|
||||||
enableBounce: Boolean = true,
|
enableBounce: Boolean = true,
|
||||||
onClickScrollOffset: Float = 300f
|
onClickScrollOffset: Float = 300f
|
||||||
@@ -88,7 +87,7 @@ fun ScrollHint(
|
|||||||
visible = visible,
|
visible = visible,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = bottomPadding),
|
.padding(bottom = dimensionResource(R.dimen.scroll_hint_padding)),
|
||||||
enter = fadeIn() + slideInVertically { it / 2 },
|
enter = fadeIn() + slideInVertically { it / 2 },
|
||||||
exit = fadeOut()
|
exit = fadeOut()
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -5,24 +5,15 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
@@ -32,13 +23,6 @@ import com.aurora.gplayapi.SearchSuggestEntry
|
|||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable for displaying search suggestions in a list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param searchSuggestEntry A [SearchSuggestEntry] to display search suggestion
|
|
||||||
* @param onClick Callback when this composable is clicked
|
|
||||||
* @param onAction Callback when action button is clicked
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchSuggestionListItem(
|
fun SearchSuggestionListItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -46,25 +30,11 @@ fun SearchSuggestionListItem(
|
|||||||
onClick: (query: String) -> Unit = {},
|
onClick: (query: String) -> Unit = {},
|
||||||
onAction: (query: String) -> Unit = {}
|
onAction: (query: String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
AuroraListItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
headline = searchSuggestEntry.title,
|
||||||
.clickable(onClick = { onClick(searchSuggestEntry.title) })
|
onClick = { onClick(searchSuggestEntry.title) },
|
||||||
.padding(
|
leading = {
|
||||||
top = dimensionResource(R.dimen.padding_xsmall),
|
|
||||||
bottom = dimensionResource(R.dimen.padding_xsmall),
|
|
||||||
start = dimensionResource(R.dimen.padding_large),
|
|
||||||
end = dimensionResource(R.dimen.padding_xsmall)
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1F)
|
|
||||||
.clickable(onClick = { onClick(searchSuggestEntry.title) }),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(
|
.data(
|
||||||
@@ -81,20 +51,16 @@ fun SearchSuggestionListItem(
|
|||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_default))
|
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_default))
|
||||||
)
|
)
|
||||||
Text(
|
},
|
||||||
text = searchSuggestEntry.title,
|
trailing = {
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
IconButton(onClick = { onAction(searchSuggestEntry.title) }) {
|
||||||
maxLines = 1,
|
Icon(
|
||||||
overflow = TextOverflow.Ellipsis
|
painter = painterResource(R.drawable.ic_search_append),
|
||||||
)
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
IconButton(onClick = { onAction(searchSuggestEntry.title) }) {
|
)
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_search_append),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
|||||||
@@ -12,15 +12,16 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
@@ -28,34 +29,30 @@ import com.aurora.store.R
|
|||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable to display sticky header in list
|
* Section header row used throughout the app. Title with optional subtitle, an optional
|
||||||
* @param modifier The modifier to be applied to the composable
|
* trailing slot, and the whole row becomes clickable when [onClick] is non-null. When
|
||||||
* @param title Title to display
|
* [trailing] is null and [onClick] is set, a default right-chevron is shown.
|
||||||
* @param subtitle Optional subtitle to display
|
|
||||||
* @param onClick Callback when this composable is clicked
|
|
||||||
* @see TextDividerComposable
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Header(
|
fun SectionHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String? = null,
|
subtitle: String? = null,
|
||||||
onClick: (() -> Unit)? = null
|
onClick: (() -> Unit)? = null,
|
||||||
|
trailing: (@Composable () -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null)
|
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
vertical = dimensionResource(R.dimen.padding_xsmall)
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
),
|
),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
modifier = Modifier.weight(1F)
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
@@ -65,15 +62,20 @@ fun Header(
|
|||||||
if (!subtitle.isNullOrBlank()) {
|
if (!subtitle.isNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = subtitle,
|
text = subtitle,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (onClick != null) {
|
when {
|
||||||
Icon(
|
trailing != null -> trailing()
|
||||||
|
onClick != null -> Icon(
|
||||||
painter = painterResource(R.drawable.ic_arrow_right),
|
painter = painterResource(R.drawable.ic_arrow_right),
|
||||||
contentDescription = null
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(dimensionResource(R.dimen.icon_size_default)),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,10 +84,23 @@ fun Header(
|
|||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun HeaderPreview() {
|
private fun SectionHeaderPreview() {
|
||||||
Header(
|
SectionHeader(title = "Top Charts", onClick = {})
|
||||||
title = stringResource(R.string.details_privacy),
|
}
|
||||||
subtitle = stringResource(R.string.exodus_powered),
|
|
||||||
onClick = {}
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun SectionHeaderWithSubtitlePreview() {
|
||||||
|
SectionHeader(title = "Permissions", subtitle = "3 requested", onClick = {})
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun SectionHeaderWithActionPreview() {
|
||||||
|
SectionHeader(
|
||||||
|
title = "3 updates available",
|
||||||
|
trailing = { TextButton(onClick = {}) { Text("Update all") } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
241
app/src/main/java/com/aurora/store/compose/composable/Shimmer.kt
Normal file
241
app/src/main/java/com/aurora/store/compose/composable/Shimmer.kt
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun shimmerBrush(): Brush {
|
||||||
|
val base = MaterialTheme.colorScheme.onSurface
|
||||||
|
val colors = listOf(
|
||||||
|
base.copy(alpha = 0.08f),
|
||||||
|
base.copy(alpha = 0.20f),
|
||||||
|
base.copy(alpha = 0.08f)
|
||||||
|
)
|
||||||
|
val transition = rememberInfiniteTransition(label = "shimmer")
|
||||||
|
val offset by transition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1000, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
),
|
||||||
|
label = "shimmer_offset"
|
||||||
|
)
|
||||||
|
val x = offset * 1200f
|
||||||
|
return Brush.linearGradient(
|
||||||
|
colors = colors,
|
||||||
|
start = Offset(x - 400f, 0f),
|
||||||
|
end = Offset(x, 400f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerBlock(modifier: Modifier, radiusRes: Int = R.dimen.radius_small) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(dimensionResource(radiusRes)))
|
||||||
|
.background(shimmerBrush())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerTextStack(modifier: Modifier = Modifier, widthFractions: List<Float>) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||||
|
) {
|
||||||
|
widthFractions.forEach { fraction ->
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(fraction)
|
||||||
|
.height(14.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerListRow(showTrailing: Boolean) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_small),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
|
) {
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
||||||
|
)
|
||||||
|
ShimmerTextStack(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
widthFractions = listOf(0.7f, 0.5f, 0.5f)
|
||||||
|
)
|
||||||
|
if (showTrailing) {
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(80.dp)
|
||||||
|
.height(36.dp),
|
||||||
|
radiusRes = R.dimen.radius_large
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ShimmerAppRow() {
|
||||||
|
ShimmerListRow(showTrailing = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ShimmerUpdateItem() {
|
||||||
|
ShimmerListRow(showTrailing = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ShimmerCategoryRow() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_small)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_large))
|
||||||
|
) {
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_category))
|
||||||
|
)
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ShimmerCarouselSection() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(all = dimensionResource(R.dimen.spacing_small)),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
|
) {
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.42f)
|
||||||
|
.height(16.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
|
) {
|
||||||
|
repeat(5) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_xsmall)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier.size(dimensionResource(R.dimen.icon_size_cluster)),
|
||||||
|
radiusRes = R.dimen.radius_medium
|
||||||
|
)
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(dimensionResource(R.dimen.icon_size_cluster) * 0.75f)
|
||||||
|
.height(11.dp)
|
||||||
|
)
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(dimensionResource(R.dimen.icon_size_cluster) * 0.5f)
|
||||||
|
.height(11.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ShimmerAppListItem() {
|
||||||
|
ShimmerBlock(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(dimensionResource(R.dimen.spacing_xsmall))
|
||||||
|
.size(dimensionResource(R.dimen.icon_size_cluster)),
|
||||||
|
radiusRes = R.dimen.radius_medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerCategoryRowPreview() {
|
||||||
|
ShimmerCategoryRow()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerAppRowPreview() {
|
||||||
|
ShimmerAppRow()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerUpdateItemPreview() {
|
||||||
|
ShimmerUpdateItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerCarouselSectionPreview() {
|
||||||
|
ShimmerCarouselSection()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerAppListItemPreview() {
|
||||||
|
ShimmerAppListItem()
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.gplayapi.data.models.StreamBundle
|
||||||
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.app.AppListItem
|
||||||
|
import com.aurora.store.compose.composable.app.LargeAppListItem
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
|
private const val LOAD_MORE_THRESHOLD = 2
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StreamCarousel(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
streamBundle: StreamBundle?,
|
||||||
|
filterSingleAppClusters: Boolean = true,
|
||||||
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
|
onHeaderClick: (StreamCluster) -> Unit = {},
|
||||||
|
onAppClick: (App) -> Unit = {},
|
||||||
|
onClusterScrolled: (StreamCluster) -> Unit = {},
|
||||||
|
onScrolledToEnd: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val bundleLoaded = streamBundle != null
|
||||||
|
LaunchedEffect(lazyListState, bundleLoaded) {
|
||||||
|
snapshotFlow {
|
||||||
|
val last = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||||
|
val total = lazyListState.layoutInfo.totalItemsCount
|
||||||
|
last >= total - LOAD_MORE_THRESHOLD
|
||||||
|
}.distinctUntilChanged().collect { reachedEnd ->
|
||||||
|
if (reachedEnd && bundleLoaded) onScrolledToEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamBundle == null) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
state = lazyListState,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
|
) {
|
||||||
|
items(5) { ShimmerCarouselSection() }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val clusters = streamBundle.streamClusters.values
|
||||||
|
.filter { cluster ->
|
||||||
|
cluster.clusterAppList.isNotEmpty() &&
|
||||||
|
cluster.clusterTitle.isNotBlank() &&
|
||||||
|
(!filterSingleAppClusters || cluster.clusterAppList.size > 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clusters.isEmpty()) {
|
||||||
|
Placeholder(
|
||||||
|
modifier = modifier,
|
||||||
|
painter = painterResource(R.drawable.ic_apps),
|
||||||
|
message = stringResource(R.string.no_apps_available)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
state = lazyListState,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(
|
||||||
|
if (clusters.size == 1) {
|
||||||
|
R.dimen.spacing_medium
|
||||||
|
} else {
|
||||||
|
R.dimen.spacing_xsmall
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (clusters.size == 1) {
|
||||||
|
val apps = clusters.first().clusterAppList
|
||||||
|
items(count = apps.size, key = { apps[it].id }) { index ->
|
||||||
|
LargeAppListItem(
|
||||||
|
app = apps[index],
|
||||||
|
onClick = { onAppClick(apps[index]) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clusters.forEach { cluster ->
|
||||||
|
item(key = "header_${cluster.id}") {
|
||||||
|
SectionHeader(
|
||||||
|
title = cluster.clusterTitle,
|
||||||
|
onClick = if (cluster.clusterBrowseUrl.isNotBlank()) {
|
||||||
|
{ onHeaderClick(cluster) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item(key = "row_${cluster.id}") {
|
||||||
|
ClusterRow(
|
||||||
|
cluster = cluster,
|
||||||
|
onAppClick = onAppClick,
|
||||||
|
onClusterScrolled = onClusterScrolled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamBundle.hasNext()) {
|
||||||
|
item(key = "shimmer_footer") { ShimmerCarouselSection() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ClusterRow(
|
||||||
|
cluster: StreamCluster,
|
||||||
|
onAppClick: (App) -> Unit = {},
|
||||||
|
onClusterScrolled: (StreamCluster) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val rowState = rememberLazyListState()
|
||||||
|
val reachedEnd by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val last = rowState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||||
|
val total = rowState.layoutInfo.totalItemsCount
|
||||||
|
last >= total - LOAD_MORE_THRESHOLD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(reachedEnd) {
|
||||||
|
if (reachedEnd && cluster.hasNext()) onClusterScrolled(cluster)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
state = rowState,
|
||||||
|
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_small)),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
|
) {
|
||||||
|
itemsIndexed(
|
||||||
|
items = cluster.clusterAppList,
|
||||||
|
key = { _, app -> app.packageName }
|
||||||
|
) { _, app ->
|
||||||
|
AppListItem(
|
||||||
|
app = app,
|
||||||
|
onClick = { onAppClick(app) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (cluster.hasNext()) {
|
||||||
|
item(key = "shimmer_${cluster.id}") { ShimmerAppListItem() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun StreamCarouselLoadingPreview() {
|
||||||
|
StreamCarousel(streamBundle = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun StreamCarouselEmptyPreview() {
|
||||||
|
StreamCarousel(streamBundle = StreamBundle.EMPTY)
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ fun TextDividerComposable(modifier: Modifier = Modifier, title: String) {
|
|||||||
Text(
|
Text(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(dimensionResource(R.dimen.padding_small)),
|
.padding(dimensionResource(R.dimen.spacing_small)),
|
||||||
text = title.uppercase(),
|
text = title.uppercase(),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
|||||||
@@ -5,11 +5,15 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
@@ -25,7 +29,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
* @param modifier The modifier to be applied to the composable
|
* @param modifier The modifier to be applied to the composable
|
||||||
* @param title Title of the screen
|
* @param title Title of the screen
|
||||||
* @param navigationIcon Icon for the navigation button
|
* @param navigationIcon Icon for the navigation button
|
||||||
* @param onNavigateUp Action when user clicks the navigation icon
|
* @param showNavigationIcon Whether to show the navigation (back) icon button
|
||||||
* @param actions Actions to display on the top app bar (for e.g. menu)
|
* @param actions Actions to display on the top app bar (for e.g. menu)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@@ -33,15 +37,17 @@ fun TopAppBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
navigationIcon: Painter = painterResource(R.drawable.ic_arrow_back),
|
navigationIcon: Painter = painterResource(R.drawable.ic_arrow_back),
|
||||||
onNavigateUp: (() -> Unit)? = null,
|
showNavigationIcon: Boolean = true,
|
||||||
|
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
|
||||||
actions: @Composable (RowScope.() -> Unit) = {}
|
actions: @Composable (RowScope.() -> Unit) = {}
|
||||||
) {
|
) {
|
||||||
|
val activity = LocalActivity.current as? ComponentActivity
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = { if (title != null) Text(text = title) },
|
title = { if (title != null) Text(text = title) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (onNavigateUp != null) {
|
if (showNavigationIcon) {
|
||||||
IconButton(onClick = onNavigateUp) {
|
IconButton(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = navigationIcon,
|
painter = navigationIcon,
|
||||||
contentDescription = stringResource(R.string.action_back)
|
contentDescription = stringResource(R.string.action_back)
|
||||||
@@ -49,6 +55,7 @@ fun TopAppBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
windowInsets = windowInsets,
|
||||||
actions = actions
|
actions = actions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -58,7 +65,6 @@ fun TopAppBar(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TopAppBarPreview() {
|
private fun TopAppBarPreview() {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = stringResource(R.string.title_about),
|
title = stringResource(R.string.title_about)
|
||||||
onNavigateUp = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ private fun AnimatedAppIconPreview(@PreviewParameter(AppPreviewProvider::class)
|
|||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun AnimatedAppIconPreview(@PreviewParameter(ProgressProvider::class) progress: Float) {
|
private fun AnimatedAppIconInProgressPreview(
|
||||||
|
@PreviewParameter(ProgressProvider::class) progress: Float
|
||||||
|
) {
|
||||||
AnimatedAppIcon(
|
AnimatedAppIcon(
|
||||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
|
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
|
||||||
iconUrl = "",
|
iconUrl = "",
|
||||||
|
|||||||
@@ -5,21 +5,24 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable.app
|
package com.aurora.store.compose.composable.app
|
||||||
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
@@ -41,38 +44,38 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) {
|
fun AppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(dimensionResource(R.dimen.icon_size_cluster))
|
.width(dimensionResource(R.dimen.icon_size_cluster))
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(dimensionResource(R.dimen.padding_xsmall))
|
.padding(all = dimensionResource(R.dimen.spacing_xsmall)),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_xsmall),
|
||||||
|
Alignment.CenterVertically
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))),
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(app.iconArtwork.url)
|
.data(app.iconArtwork.url)
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop
|
||||||
|
|
||||||
|
)
|
||||||
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.requiredSize(dimensionResource(R.dimen.icon_size_cluster))
|
.fillMaxWidth(),
|
||||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = app.displayName,
|
text = app.displayName,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
textAlign = TextAlign.Start
|
||||||
Text(
|
|
||||||
text = if (app.size > 0) {
|
|
||||||
Formatter.formatShortFileSize(context, app.size)
|
|
||||||
} else {
|
|
||||||
app.downloadString
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable.app
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
|
import com.aurora.store.data.room.download.Download
|
||||||
|
import com.aurora.store.data.room.update.Update
|
||||||
|
import com.aurora.store.util.CommonUtil
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppUpdateItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
update: Update,
|
||||||
|
download: Download? = null,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
onUpdate: () -> Unit = {},
|
||||||
|
onCancel: () -> Unit = {},
|
||||||
|
onUnignore: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val inProgress = download != null && !download.isFinished
|
||||||
|
val pendingInstall = download?.status == DownloadStatus.COMPLETED
|
||||||
|
val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
|
||||||
|
download.progress.toFloat()
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_small)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium))) {
|
||||||
|
AnimatedAppIcon(
|
||||||
|
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
|
||||||
|
iconUrl = update.iconURL,
|
||||||
|
inProgress = inProgress || pendingInstall,
|
||||||
|
progress = progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(dimensionResource(R.dimen.spacing_medium)))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = update.displayName,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = update.developerName,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${update.versionName} • ${CommonUtil.addSiPrefix(update.size)}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(dimensionResource(R.dimen.spacing_small)))
|
||||||
|
when {
|
||||||
|
onUnignore != null -> {
|
||||||
|
OutlinedButton(onClick = onUnignore) {
|
||||||
|
Text(stringResource(R.string.action_unignore))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingInstall -> {
|
||||||
|
OutlinedButton(onClick = {}, enabled = false) {
|
||||||
|
Text(stringResource(R.string.action_installing))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inProgress -> {
|
||||||
|
OutlinedButton(onClick = onCancel) {
|
||||||
|
Text(stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Button(onClick = onUpdate) {
|
||||||
|
Text(stringResource(R.string.action_update))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppUpdateItemPreview() {
|
||||||
|
val update = Update(
|
||||||
|
packageName = "com.example.app",
|
||||||
|
versionCode = 100,
|
||||||
|
versionName = "1.0.0",
|
||||||
|
displayName = "Example App",
|
||||||
|
iconURL = "",
|
||||||
|
changelog = "• Bug fixes<br>• Performance improvements",
|
||||||
|
id = 1,
|
||||||
|
developerName = "Developer",
|
||||||
|
size = 5_000_000,
|
||||||
|
updatedOn = "May 2025",
|
||||||
|
hasValidCert = true,
|
||||||
|
offerType = 1,
|
||||||
|
fileList = emptyList(),
|
||||||
|
sharedLibs = emptyList()
|
||||||
|
)
|
||||||
|
AppUpdateItem(update = update)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppUpdateItemDownloadingPreview() {
|
||||||
|
val update = Update(
|
||||||
|
packageName = "com.example.app",
|
||||||
|
versionCode = 100,
|
||||||
|
versionName = "1.0.0",
|
||||||
|
displayName = "Example App",
|
||||||
|
iconURL = "",
|
||||||
|
changelog = "",
|
||||||
|
id = 1,
|
||||||
|
developerName = "Developer",
|
||||||
|
size = 5_000_000,
|
||||||
|
updatedOn = "May 2025",
|
||||||
|
hasValidCert = true,
|
||||||
|
offerType = 1,
|
||||||
|
fileList = emptyList(),
|
||||||
|
sharedLibs = emptyList()
|
||||||
|
)
|
||||||
|
val download = Download.fromUpdate(update).copy(
|
||||||
|
status = DownloadStatus.DOWNLOADING,
|
||||||
|
progress = 45
|
||||||
|
)
|
||||||
|
AppUpdateItem(update = update, download = download)
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable.app
|
||||||
|
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.AuroraListItem
|
||||||
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import com.aurora.store.util.CommonUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List item for the installed-apps screen. Mirrors [LargeAppListItem] but the tertiary
|
||||||
|
* line shows installation-relevant info (APK size, last update, installer) rather than
|
||||||
|
* download / rating / price / ads which only matter on Play listings.
|
||||||
|
*
|
||||||
|
* Each tertiary segment is omitted if the corresponding value is missing, so the row
|
||||||
|
* gracefully collapses when only some metadata is available.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun InstalledAppListItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
app: App,
|
||||||
|
sizeBytes: Long = 0L,
|
||||||
|
lastUpdateTime: Long = 0L,
|
||||||
|
installerLabel: String? = null,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
AuroraListItem(
|
||||||
|
modifier = modifier,
|
||||||
|
headline = app.displayName,
|
||||||
|
supporting = app.packageName,
|
||||||
|
tertiary = buildInstalledExtras(sizeBytes, lastUpdateTime, installerLabel),
|
||||||
|
headlineStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
onClick = onClick,
|
||||||
|
leading = {
|
||||||
|
AsyncImage(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
||||||
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))),
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(app.iconArtwork.url)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun buildInstalledExtras(
|
||||||
|
sizeBytes: Long,
|
||||||
|
lastUpdateTime: Long,
|
||||||
|
installerLabel: String?
|
||||||
|
): String = buildList {
|
||||||
|
if (sizeBytes > 0) add(CommonUtil.addSiPrefix(sizeBytes))
|
||||||
|
if (lastUpdateTime > 0) {
|
||||||
|
val relative = DateUtils.getRelativeTimeSpanString(
|
||||||
|
lastUpdateTime,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
DateUtils.DAY_IN_MILLIS
|
||||||
|
)
|
||||||
|
add(stringResource(R.string.installed_updated_at, relative))
|
||||||
|
}
|
||||||
|
if (!installerLabel.isNullOrBlank()) add(installerLabel)
|
||||||
|
}.joinToString(separator = " • ")
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun InstalledAppListItemPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
InstalledAppListItem(
|
||||||
|
app = app,
|
||||||
|
sizeBytes = 54_300_000L,
|
||||||
|
lastUpdateTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS * 5,
|
||||||
|
installerLabel = "Aurora Store"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,15 +5,9 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable.app
|
package com.aurora.store.compose.composable.app
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -21,7 +15,6 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
@@ -31,79 +24,49 @@ import coil3.request.crossfade
|
|||||||
import com.aurora.extensions.requiresGMS
|
import com.aurora.extensions.requiresGMS
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.AuroraListItem
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.util.CommonUtil
|
import com.aurora.store.util.CommonUtil
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable for displaying minimal app details in a vertical-scrollable list
|
|
||||||
* @param modifier The modifier to be applied to the composable
|
|
||||||
* @param app [App] to display
|
|
||||||
* @param onClick Callback when the composable is clicked
|
|
||||||
* @see AppListItem
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LargeAppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) {
|
fun LargeAppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) {
|
||||||
@Composable
|
AuroraListItem(
|
||||||
fun buildExtras(app: App): List<String> = mutableListOf<String>().apply {
|
modifier = modifier,
|
||||||
add(if (app.size > 0) CommonUtil.addSiPrefix(app.size) else app.downloadString)
|
headline = app.displayName,
|
||||||
add("${app.labeledRating}★")
|
supporting = app.developerName,
|
||||||
|
tertiary = buildAppExtras(app),
|
||||||
if (app.isFree) {
|
headlineStyle = MaterialTheme.typography.bodyMedium,
|
||||||
add(stringResource(R.string.details_free))
|
onClick = onClick,
|
||||||
} else {
|
leading = {
|
||||||
add(stringResource(R.string.details_paid))
|
AsyncImage(
|
||||||
}
|
modifier = Modifier
|
||||||
|
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
||||||
if (app.containsAds) {
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))),
|
||||||
add(stringResource(R.string.details_contains_ads))
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
} else {
|
.data(app.iconArtwork.url)
|
||||||
add(stringResource(R.string.details_no_ads))
|
.crossfade(true)
|
||||||
}
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
if (app.requiresGMS()) {
|
contentScale = ContentScale.Crop
|
||||||
add(stringResource(R.string.details_gsf_dependent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(
|
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
|
||||||
vertical = dimensionResource(R.dimen.padding_small)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(app.iconArtwork.url)
|
|
||||||
.crossfade(true)
|
|
||||||
.build(),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
|
||||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = app.displayName,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(text = app.developerName, style = MaterialTheme.typography.bodySmall)
|
|
||||||
Text(
|
|
||||||
text = buildExtras(app).joinToString(separator = " • "),
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun buildAppExtras(app: App): String = buildList {
|
||||||
|
add(if (app.size > 0) CommonUtil.addSiPrefix(app.size) else app.downloadString)
|
||||||
|
add("${app.labeledRating}★")
|
||||||
|
add(stringResource(if (app.isFree) R.string.details_free else R.string.details_paid))
|
||||||
|
add(
|
||||||
|
stringResource(
|
||||||
|
if (app.containsAds) R.string.details_contains_ads else R.string.details_no_ads
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (app.requiresGMS()) add(stringResource(R.string.details_gsf_dependent))
|
||||||
|
}.joinToString(separator = " • ")
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ fun ExodusListItem(modifier: Modifier = Modifier, tracker: ExodusTracker) {
|
|||||||
onClick = { context.browse(tracker.url) },
|
onClick = { context.browse(tracker.url) },
|
||||||
enabled = tracker.url.isNotBlank()
|
enabled = tracker.url.isNotBlank()
|
||||||
)
|
)
|
||||||
.padding(dimensionResource(R.dimen.padding_small))
|
.padding(dimensionResource(R.dimen.spacing_small))
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = tracker.name,
|
text = tracker.name,
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ fun RatingListItem(modifier: Modifier = Modifier, label: String, rating: Float)
|
|||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = dimensionResource(R.dimen.padding_small)),
|
.padding(horizontal = dimensionResource(R.dimen.spacing_small)),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ fun ReviewListItem(modifier: Modifier = Modifier, review: Review) {
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
vertical = dimensionResource(R.dimen.padding_small)
|
vertical = dimensionResource(R.dimen.spacing_small)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
@@ -61,7 +61,7 @@ fun ReviewListItem(modifier: Modifier = Modifier, review: Review) {
|
|||||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_small))
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = review.userName,
|
text = review.userName,
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composition
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.neverEqualPolicy
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects a [Flow] as [State] starting from [initial], always triggering recomposition on
|
||||||
|
* every emission regardless of structural equality, by using [neverEqualPolicy].
|
||||||
|
*
|
||||||
|
* Useful for non-state flows (e.g. [kotlinx.coroutines.flow.SharedFlow]) and for flows whose
|
||||||
|
* values have broken `equals` implementations that would otherwise be conflated upstream.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun <T> Flow<T>.collectForced(initial: T): State<T> {
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
val state: MutableState<T> = remember(this) { mutableStateOf(initial, neverEqualPolicy()) }
|
||||||
|
LaunchedEffect(this, lifecycleOwner) {
|
||||||
|
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
collect { state.value = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observes a [LiveData] as [State], always triggering recomposition on every emission
|
||||||
|
* regardless of structural equality, by using [neverEqualPolicy].
|
||||||
|
*/
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@Composable
|
||||||
|
internal fun <T> LiveData<T>.observeForced(): State<T?> {
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
val state: MutableState<T?> = remember { mutableStateOf(value, neverEqualPolicy()) }
|
||||||
|
DisposableEffect(this, lifecycleOwner) {
|
||||||
|
val observer = Observer<T> { state.value = it }
|
||||||
|
observe(lifecycleOwner, observer)
|
||||||
|
onDispose { removeObserver(observer) }
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composition
|
||||||
|
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import com.aurora.store.data.model.NetworkStatus
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CompositionLocal carrying the current device network status. Provided once at the
|
||||||
|
* activity root from a single [com.aurora.store.data.providers.NetworkProvider] subscription,
|
||||||
|
* so any screen can read `LocalNetworkStatus.current` without injecting the provider
|
||||||
|
* or duplicating the flow collection.
|
||||||
|
*
|
||||||
|
* Uses [compositionLocalOf] (not static) so only readers recompose on change.
|
||||||
|
*/
|
||||||
|
val LocalNetworkStatus = compositionLocalOf { NetworkStatus.AVAILABLE }
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.navigation
|
||||||
|
|
||||||
|
import com.aurora.gplayapi.data.models.Category
|
||||||
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
|
import com.aurora.store.data.model.PermissionType
|
||||||
|
import com.aurora.store.data.room.update.Update
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All navigation actions available to composable screens.
|
||||||
|
* Screens emit one of these via a single `onNavigateTo: (Destination) -> Unit` callback.
|
||||||
|
*/
|
||||||
|
sealed class Destination {
|
||||||
|
data object Splash : Destination()
|
||||||
|
data class Main(val initialTab: Int) : Destination()
|
||||||
|
|
||||||
|
data class AppDetails(val packageName: String) : Destination()
|
||||||
|
data class DevProfile(val devId: String) : Destination()
|
||||||
|
data class AppUpdate(val update: Update) : Destination()
|
||||||
|
|
||||||
|
data object Search : Destination()
|
||||||
|
data object Downloads : Destination()
|
||||||
|
|
||||||
|
data class StreamBrowse(val cluster: StreamCluster) : Destination()
|
||||||
|
data class ExpandedStreamBrowse(val title: String, val browseUrl: String) : Destination()
|
||||||
|
data class CategoryBrowse(val category: Category) : Destination()
|
||||||
|
data class PermissionRationale(val permissions: Set<PermissionType>) : Destination()
|
||||||
|
|
||||||
|
data object Accounts : Destination()
|
||||||
|
data object GoogleLogin : Destination()
|
||||||
|
data object About : Destination()
|
||||||
|
data object Favourite : Destination()
|
||||||
|
data object Spoof : Destination()
|
||||||
|
data object Installed : Destination()
|
||||||
|
data object Blacklist : Destination()
|
||||||
|
|
||||||
|
data object Settings : Destination()
|
||||||
|
data object InstallationPreference : Destination()
|
||||||
|
data object Installer : Destination()
|
||||||
|
data object NetworkPreference : Destination()
|
||||||
|
data object Dispenser : Destination()
|
||||||
|
data object UIPreference : Destination()
|
||||||
|
data object UpdatesPreference : Destination()
|
||||||
|
data object SourceFilters : Destination()
|
||||||
|
}
|
||||||
@@ -7,16 +7,23 @@
|
|||||||
package com.aurora.store.compose.navigation
|
package com.aurora.store.compose.navigation
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.compose.LocalActivity
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
import androidx.navigation.NavDeepLinkBuilder
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
|
import androidx.navigation3.runtime.metadata
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
@@ -24,11 +31,13 @@ import com.aurora.Constants.PACKAGE_NAME_GMS
|
|||||||
import com.aurora.extensions.toast
|
import com.aurora.extensions.toast
|
||||||
import com.aurora.store.AuroraApp
|
import com.aurora.store.AuroraApp
|
||||||
import com.aurora.store.ComposeActivity
|
import com.aurora.store.ComposeActivity
|
||||||
import com.aurora.store.MainActivity
|
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.ui.about.AboutScreen
|
import com.aurora.store.compose.ui.about.AboutScreen
|
||||||
import com.aurora.store.compose.ui.accounts.AccountsScreen
|
import com.aurora.store.compose.ui.accounts.AccountsScreen
|
||||||
|
import com.aurora.store.compose.ui.accounts.GoogleLoginScreen
|
||||||
import com.aurora.store.compose.ui.blacklist.BlacklistScreen
|
import com.aurora.store.compose.ui.blacklist.BlacklistScreen
|
||||||
|
import com.aurora.store.compose.ui.commons.CategoryBrowseScreen
|
||||||
|
import com.aurora.store.compose.ui.commons.ExpandedStreamBrowseScreen
|
||||||
import com.aurora.store.compose.ui.commons.PermissionRationaleScreen
|
import com.aurora.store.compose.ui.commons.PermissionRationaleScreen
|
||||||
import com.aurora.store.compose.ui.commons.StreamBrowseScreen
|
import com.aurora.store.compose.ui.commons.StreamBrowseScreen
|
||||||
import com.aurora.store.compose.ui.details.AppDetailsScreen
|
import com.aurora.store.compose.ui.details.AppDetailsScreen
|
||||||
@@ -37,9 +46,17 @@ import com.aurora.store.compose.ui.dispenser.DispenserScreen
|
|||||||
import com.aurora.store.compose.ui.downloads.DownloadsScreen
|
import com.aurora.store.compose.ui.downloads.DownloadsScreen
|
||||||
import com.aurora.store.compose.ui.favourite.FavouriteScreen
|
import com.aurora.store.compose.ui.favourite.FavouriteScreen
|
||||||
import com.aurora.store.compose.ui.installed.InstalledScreen
|
import com.aurora.store.compose.ui.installed.InstalledScreen
|
||||||
|
import com.aurora.store.compose.ui.main.MainScreen
|
||||||
import com.aurora.store.compose.ui.onboarding.OnboardingScreen
|
import com.aurora.store.compose.ui.onboarding.OnboardingScreen
|
||||||
|
import com.aurora.store.compose.ui.preferences.SettingsScreen
|
||||||
|
import com.aurora.store.compose.ui.preferences.UIPreferenceScreen
|
||||||
|
import com.aurora.store.compose.ui.preferences.installation.InstallationPreferenceScreen
|
||||||
import com.aurora.store.compose.ui.preferences.installation.InstallerScreen
|
import com.aurora.store.compose.ui.preferences.installation.InstallerScreen
|
||||||
|
import com.aurora.store.compose.ui.preferences.network.NetworkPreferenceScreen
|
||||||
|
import com.aurora.store.compose.ui.preferences.updates.SourceFiltersScreen
|
||||||
|
import com.aurora.store.compose.ui.preferences.updates.UpdatesPreferenceScreen
|
||||||
import com.aurora.store.compose.ui.search.SearchScreen
|
import com.aurora.store.compose.ui.search.SearchScreen
|
||||||
|
import com.aurora.store.compose.ui.splash.SplashScreen
|
||||||
import com.aurora.store.compose.ui.spoof.SpoofScreen
|
import com.aurora.store.compose.ui.spoof.SpoofScreen
|
||||||
import com.aurora.store.data.event.InstallerEvent
|
import com.aurora.store.data.event.InstallerEvent
|
||||||
import com.aurora.store.data.model.AccountType
|
import com.aurora.store.data.model.AccountType
|
||||||
@@ -54,23 +71,6 @@ import com.aurora.store.util.Preferences
|
|||||||
@Composable
|
@Composable
|
||||||
fun NavDisplay(startDestination: NavKey) {
|
fun NavDisplay(startDestination: NavKey) {
|
||||||
val backstack = rememberNavBackStack(startDestination)
|
val backstack = rememberNavBackStack(startDestination)
|
||||||
|
|
||||||
// TODO: Rework when migrating splash fragment to compose
|
|
||||||
val splashIntent = NavDeepLinkBuilder(LocalContext.current)
|
|
||||||
.setGraph(R.navigation.mobile_navigation)
|
|
||||||
.setDestination(R.id.splashFragment)
|
|
||||||
.setComponentName(MainActivity::class.java)
|
|
||||||
.createTaskStackBuilder()
|
|
||||||
.intents
|
|
||||||
.first()
|
|
||||||
.apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) }
|
|
||||||
|
|
||||||
// TODO: Drop this logic once everything is in compose
|
|
||||||
val activity = LocalActivity.current
|
|
||||||
fun onNavigateUp() {
|
|
||||||
if (backstack.size == 1) activity?.finish() else backstack.removeLastOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
fun isMicroGAuthInvalidated(): Boolean =
|
fun isMicroGAuthInvalidated(): Boolean =
|
||||||
@@ -101,114 +101,171 @@ fun NavDisplay(startDestination: NavKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun navigate(destination: Destination) {
|
||||||
|
when (destination) {
|
||||||
|
Destination.Splash -> {
|
||||||
|
// Clear the backstack when navigating to Splash to prevent going back to the previous screen when the user is sent back to the splash screen (e.g. after logout).
|
||||||
|
backstack.clear()
|
||||||
|
backstack.add(Screen.Splash)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Destination.Main -> {
|
||||||
|
// Clear the backstack when navigating to Main to prevent going back to the splash screen or other screens.
|
||||||
|
backstack.clear()
|
||||||
|
backstack.add(Screen.Main(destination.initialTab))
|
||||||
|
}
|
||||||
|
|
||||||
|
is Destination.ExpandedStreamBrowse -> backstack.add(
|
||||||
|
Screen.ExpandedStreamBrowse(destination.title, destination.browseUrl)
|
||||||
|
)
|
||||||
|
|
||||||
|
is Destination.CategoryBrowse -> backstack.add(
|
||||||
|
Screen.CategoryBrowse(destination.category.title, destination.category.browseUrl)
|
||||||
|
)
|
||||||
|
|
||||||
|
is Destination.PermissionRationale -> backstack.add(
|
||||||
|
Screen.PermissionRationale(destination.permissions)
|
||||||
|
)
|
||||||
|
|
||||||
|
is Destination.AppDetails -> backstack.add(Screen.AppDetails(destination.packageName))
|
||||||
|
is Destination.DevProfile -> backstack.add(Screen.DevProfile(destination.devId))
|
||||||
|
is Destination.AppUpdate -> Unit
|
||||||
|
is Destination.StreamBrowse -> backstack.add(Screen.StreamBrowse(destination.cluster))
|
||||||
|
|
||||||
|
Destination.Search -> backstack.add(Screen.Search)
|
||||||
|
Destination.Downloads -> backstack.add(Screen.Downloads)
|
||||||
|
Destination.Accounts -> backstack.add(Screen.Accounts)
|
||||||
|
Destination.GoogleLogin -> backstack.add(Screen.GoogleLogin)
|
||||||
|
Destination.About -> backstack.add(Screen.About)
|
||||||
|
Destination.Favourite -> backstack.add(Screen.Favourite)
|
||||||
|
Destination.Spoof -> backstack.add(Screen.Spoof)
|
||||||
|
Destination.Installed -> backstack.add(Screen.Installed)
|
||||||
|
Destination.Blacklist -> backstack.add(Screen.Blacklist)
|
||||||
|
Destination.Settings -> backstack.add(Screen.Settings)
|
||||||
|
Destination.InstallationPreference -> backstack.add(Screen.InstallationPreference)
|
||||||
|
Destination.Installer -> backstack.add(Screen.Installer)
|
||||||
|
Destination.NetworkPreference -> backstack.add(Screen.NetworkPreference)
|
||||||
|
Destination.Dispenser -> backstack.add(Screen.Dispenser)
|
||||||
|
Destination.UIPreference -> backstack.add(Screen.UIPreference)
|
||||||
|
Destination.UpdatesPreference -> backstack.add(Screen.UpdatesPreference)
|
||||||
|
Destination.SourceFilters -> backstack.add(Screen.SourceFilters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
|
onBack = { backstack.removeLastOrNull() },
|
||||||
backStack = backstack,
|
backStack = backstack,
|
||||||
entryDecorators = listOf(
|
entryDecorators = listOf(
|
||||||
rememberSaveableStateHolderNavEntryDecorator(),
|
rememberSaveableStateHolderNavEntryDecorator(),
|
||||||
rememberViewModelStoreNavEntryDecorator()
|
rememberViewModelStoreNavEntryDecorator()
|
||||||
),
|
),
|
||||||
|
transitionSpec = {
|
||||||
|
slideInHorizontally(
|
||||||
|
animationSpec = spring(dampingRatio = 0.8f, stiffness = 380f),
|
||||||
|
initialOffsetX = { it }
|
||||||
|
) togetherWith slideOutHorizontally(targetOffsetX = { -it })
|
||||||
|
},
|
||||||
|
popTransitionSpec = {
|
||||||
|
slideInHorizontally(
|
||||||
|
animationSpec = spring(dampingRatio = 0.8f, stiffness = 380f),
|
||||||
|
initialOffsetX = { -it }
|
||||||
|
) togetherWith slideOutHorizontally(targetOffsetX = { it })
|
||||||
|
},
|
||||||
|
predictivePopTransitionSpec = {
|
||||||
|
slideInHorizontally(
|
||||||
|
animationSpec = spring(dampingRatio = 0.8f, stiffness = 380f),
|
||||||
|
initialOffsetX = { -it }
|
||||||
|
) togetherWith slideOutHorizontally(targetOffsetX = { it })
|
||||||
|
},
|
||||||
entryProvider = entryProvider {
|
entryProvider = entryProvider {
|
||||||
entry<Screen.Blacklist> {
|
entry<Screen.Main> { screen ->
|
||||||
BlacklistScreen(onNavigateUp = ::onNavigateUp)
|
MainScreen(
|
||||||
}
|
initialTab = screen.initialTab,
|
||||||
|
onNavigateTo = ::navigate
|
||||||
entry<Screen.Search> {
|
)
|
||||||
SearchScreen(onNavigateUp = ::onNavigateUp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<Screen.AppDetails> { screen ->
|
entry<Screen.AppDetails> { screen ->
|
||||||
AppDetailsScreen(
|
AppDetailsScreen(
|
||||||
packageName = screen.packageName,
|
packageName = screen.packageName,
|
||||||
onNavigateUp = ::onNavigateUp,
|
onNavigateTo = ::navigate
|
||||||
onNavigateToAppDetails = { packageName ->
|
|
||||||
backstack.add(Screen.AppDetails(packageName))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<Screen.DevProfile> { screen ->
|
entry<Screen.DevProfile> { screen ->
|
||||||
DevProfileScreen(
|
DevProfileScreen(
|
||||||
developerId = screen.developerId,
|
developerId = screen.developerId,
|
||||||
onNavigateUp = ::onNavigateUp,
|
onNavigateTo = ::navigate
|
||||||
onNavigateToAppDetails = { packageName ->
|
|
||||||
backstack.add(Screen.AppDetails(packageName))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<Screen.PermissionRationale> { screen ->
|
entry<Screen.PermissionRationale> { screen ->
|
||||||
PermissionRationaleScreen(
|
PermissionRationaleScreen(
|
||||||
onNavigateUp = ::onNavigateUp,
|
|
||||||
requiredPermissions = screen.requiredPermissions
|
requiredPermissions = screen.requiredPermissions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<Screen.Downloads> {
|
|
||||||
DownloadsScreen(
|
|
||||||
onNavigateUp = ::onNavigateUp,
|
|
||||||
onNavigateToAppDetails = { packageName ->
|
|
||||||
backstack.add(Screen.AppDetails(packageName))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Accounts> {
|
|
||||||
AccountsScreen(
|
|
||||||
onNavigateUp = ::onNavigateUp,
|
|
||||||
onNavigateToSplash = { activity?.startActivity(splashIntent) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.About> {
|
|
||||||
AboutScreen(onNavigateUp = ::onNavigateUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Favourite> {
|
|
||||||
FavouriteScreen(
|
|
||||||
onNavigateUp = ::onNavigateUp,
|
|
||||||
onNavigateToAppDetails = { packageName ->
|
|
||||||
backstack.add(Screen.AppDetails(packageName))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Onboarding> {
|
|
||||||
OnboardingScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Spoof> {
|
|
||||||
SpoofScreen(
|
|
||||||
onNavigateUp = ::onNavigateUp,
|
|
||||||
onNavigateToSplash = { activity?.startActivity(splashIntent) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Dispenser> {
|
|
||||||
DispenserScreen(onNavigateUp = ::onNavigateUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Installer> {
|
|
||||||
InstallerScreen(onNavigateUp = ::onNavigateUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Installed> {
|
|
||||||
InstalledScreen(
|
|
||||||
onNavigateUp = ::onNavigateUp,
|
|
||||||
onNavigateToAppDetails = { packageName ->
|
|
||||||
backstack.add(Screen.AppDetails(packageName))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.StreamBrowse> { screen ->
|
entry<Screen.StreamBrowse> { screen ->
|
||||||
StreamBrowseScreen(
|
StreamBrowseScreen(
|
||||||
streamCluster = screen.streamCluster,
|
streamCluster = screen.streamCluster,
|
||||||
onNavigateUp = ::onNavigateUp,
|
onNavigateTo = ::navigate
|
||||||
onNavigateToAppDetails = { packageName ->
|
|
||||||
backstack.add(Screen.AppDetails(packageName))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<Screen.ExpandedStreamBrowse> { screen ->
|
||||||
|
ExpandedStreamBrowseScreen(
|
||||||
|
browseUrl = screen.browseUrl,
|
||||||
|
defaultTitle = screen.title,
|
||||||
|
onNavigateTo = ::navigate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<Screen.CategoryBrowse> { screen ->
|
||||||
|
CategoryBrowseScreen(
|
||||||
|
title = screen.title,
|
||||||
|
browseUrl = screen.browseUrl,
|
||||||
|
onNavigateTo = ::navigate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<Screen.InstallationPreference> {
|
||||||
|
InstallationPreferenceScreen(onNavigateTo = ::navigate)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<Screen.Search>(
|
||||||
|
metadata = metadata {
|
||||||
|
put(NavDisplay.TransitionKey) {
|
||||||
|
fadeIn() togetherWith
|
||||||
|
ExitTransition.KeepUntilTransitionsFinished
|
||||||
|
}
|
||||||
|
put(NavDisplay.PopTransitionKey) {
|
||||||
|
EnterTransition.None togetherWith
|
||||||
|
slideOutVertically(targetOffsetY = { it })
|
||||||
|
}
|
||||||
|
put(NavDisplay.PredictivePopTransitionKey) {
|
||||||
|
EnterTransition.None togetherWith
|
||||||
|
slideOutVertically(targetOffsetY = { it })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { SearchScreen() }
|
||||||
|
|
||||||
|
entry<Screen.Splash> { SplashScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.Onboarding> { OnboardingScreen() }
|
||||||
|
entry<Screen.Blacklist> { BlacklistScreen() }
|
||||||
|
entry<Screen.Downloads> { DownloadsScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.Accounts> { AccountsScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.GoogleLogin> { GoogleLoginScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.About> { AboutScreen() }
|
||||||
|
entry<Screen.Favourite> { FavouriteScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.Spoof> { SpoofScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.Dispenser> { DispenserScreen() }
|
||||||
|
entry<Screen.Installer> { InstallerScreen() }
|
||||||
|
entry<Screen.Installed> { InstalledScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.Settings> { SettingsScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.NetworkPreference> { NetworkPreferenceScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.UIPreference> { UIPreferenceScreen() }
|
||||||
|
entry<Screen.UpdatesPreference> { UpdatesPreferenceScreen(onNavigateTo = ::navigate) }
|
||||||
|
entry<Screen.SourceFilters> { SourceFiltersScreen() }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ sealed class Screen : NavKey, Parcelable {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data object Accounts : Screen()
|
data object Accounts : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object GoogleLogin : Screen()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object About : Screen()
|
data object About : Screen()
|
||||||
|
|
||||||
@@ -67,4 +70,34 @@ sealed class Screen : NavKey, Parcelable {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StreamBrowse(val streamCluster: StreamCluster) : Screen()
|
data class StreamBrowse(val streamCluster: StreamCluster) : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExpandedStreamBrowse(val title: String, val browseUrl: String) : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CategoryBrowse(val title: String, val browseUrl: String) : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Settings : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object InstallationPreference : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object NetworkPreference : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object UIPreference : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object UpdatesPreference : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SourceFilters : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Splash : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Main(val initialTab: Int = 0) : Screen()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
|
|
||||||
val warningColor = Color(0xFFFF7600) // Amber
|
val warningColor = Color(0xFFFF7600) // Amber
|
||||||
val successColor = Color(0xFF1B8738) // Green
|
val successColor = Color(0xFF1B8738) // Green
|
||||||
|
|
||||||
|
val colorGreen = Color(0xFF4CAF50)
|
||||||
|
val colorRed = Color(0xFFF44336)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.theme
|
package com.aurora.store.compose.theme
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.compose.LocalActivity
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -16,7 +17,12 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
|||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
@@ -31,7 +37,19 @@ import com.aurora.store.util.Preferences
|
|||||||
fun AuroraTheme(content: @Composable () -> Unit) {
|
fun AuroraTheme(content: @Composable () -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val themeStyle = Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE)
|
var themeStyle by remember {
|
||||||
|
mutableIntStateOf(Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE))
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
val prefs = Preferences.getPrefs(context)
|
||||||
|
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||||
|
if (key == Preferences.PREFERENCE_THEME_STYLE) {
|
||||||
|
themeStyle = Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||||
|
onDispose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||||
|
}
|
||||||
val isDynamicColorSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
val isDynamicColorSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
|
|
||||||
val lightScheme = if (isDynamicColorSupported) {
|
val lightScheme = if (isDynamicColorSupported) {
|
||||||
@@ -69,11 +87,9 @@ fun AuroraTheme(content: @Composable () -> Unit) {
|
|||||||
val currentActivity = activity ?: return@SideEffect
|
val currentActivity = activity ?: return@SideEffect
|
||||||
val window = currentActivity.window
|
val window = currentActivity.window
|
||||||
|
|
||||||
// Transparent system bars
|
|
||||||
window.statusBarColor = android.graphics.Color.TRANSPARENT
|
window.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||||
window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
||||||
|
|
||||||
// Control icon colors explicitly
|
|
||||||
WindowCompat
|
WindowCompat
|
||||||
.getInsetsController(window, view)
|
.getInsetsController(window, view)
|
||||||
.apply {
|
.apply {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
import com.aurora.store.data.model.Link
|
import com.aurora.store.data.model.Link
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutScreen(onNavigateUp: () -> Unit) {
|
fun AboutScreen() {
|
||||||
var shouldShowAboutDialog by rememberSaveable { mutableStateOf(false) }
|
var shouldShowAboutDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
if (shouldShowAboutDialog) {
|
if (shouldShowAboutDialog) {
|
||||||
@@ -54,13 +54,12 @@ fun AboutScreen(onNavigateUp: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
onAboutAurora = { shouldShowAboutDialog = true }
|
onAboutAurora = { shouldShowAboutDialog = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(onNavigateUp: () -> Unit = {}, onAboutAurora: () -> Unit = {}) {
|
private fun ScreenContent(onAboutAurora: () -> Unit = {}) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val linkURLS = stringArrayResource(R.array.link_urls)
|
val linkURLS = stringArrayResource(R.array.link_urls)
|
||||||
@@ -94,8 +93,7 @@ private fun ScreenContent(onNavigateUp: () -> Unit = {}, onAboutAurora: () -> Un
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = stringResource(R.string.title_about),
|
title = stringResource(R.string.title_about)
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -103,7 +101,7 @@ private fun ScreenContent(onNavigateUp: () -> Unit = {}, onAboutAurora: () -> Un
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues),
|
.padding(paddingValues),
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||||
) {
|
) {
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Surface(modifier = Modifier.fillMaxWidth()) {
|
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||||
@@ -130,9 +128,9 @@ private fun BrandHeader() {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(dimensionResource(R.dimen.margin_medium)),
|
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
|||||||
@@ -48,19 +48,18 @@ import com.aurora.Constants.URL_TOS
|
|||||||
import com.aurora.extensions.browse
|
import com.aurora.extensions.browse
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.data.providers.AccountProvider
|
import com.aurora.store.data.providers.AccountProvider
|
||||||
import com.aurora.store.viewmodel.accounts.AccountsViewModel
|
import com.aurora.store.viewmodel.accounts.AccountsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountsScreen(
|
fun AccountsScreen(
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateTo: (Destination) -> Unit,
|
||||||
onNavigateToSplash: () -> Unit,
|
|
||||||
viewModel: AccountsViewModel = hiltViewModel()
|
viewModel: AccountsViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val userProfile = viewModel.authProvider.authData?.userProfile
|
val userProfile = viewModel.authProvider.authData?.userProfile
|
||||||
val avatar = when {
|
val avatar = when {
|
||||||
viewModel.authProvider.isAnonymous -> R.mipmap.ic_launcher
|
|
||||||
userProfile != null -> userProfile.artwork.url
|
userProfile != null -> userProfile.artwork.url
|
||||||
else -> R.mipmap.ic_launcher
|
else -> R.mipmap.ic_launcher
|
||||||
}
|
}
|
||||||
@@ -70,7 +69,6 @@ fun AccountsScreen(
|
|||||||
else -> stringResource(R.string.account_anonymous)
|
else -> stringResource(R.string.account_anonymous)
|
||||||
}
|
}
|
||||||
val email = when {
|
val email = when {
|
||||||
viewModel.authProvider.isAnonymous -> stringResource(R.string.account_anonymous_email)
|
|
||||||
userProfile != null -> userProfile.email
|
userProfile != null -> userProfile.email
|
||||||
else -> stringResource(R.string.account_anonymous_email)
|
else -> stringResource(R.string.account_anonymous_email)
|
||||||
}
|
}
|
||||||
@@ -83,14 +81,13 @@ fun AccountsScreen(
|
|||||||
onConfirm = {
|
onConfirm = {
|
||||||
shouldShowLogoutDialog = false
|
shouldShowLogoutDialog = false
|
||||||
AccountProvider.logout(context)
|
AccountProvider.logout(context)
|
||||||
onNavigateToSplash()
|
onNavigateTo(Destination.Splash)
|
||||||
},
|
},
|
||||||
onDismiss = { shouldShowLogoutDialog = false }
|
onDismiss = { shouldShowLogoutDialog = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
name = name,
|
name = name,
|
||||||
email = email,
|
email = email,
|
||||||
@@ -100,17 +97,15 @@ fun AccountsScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
avatar: Any = R.mipmap.ic_launcher,
|
avatar: Any = R.drawable.ic_account,
|
||||||
name: String = stringResource(R.string.account_anonymous),
|
name: String = stringResource(R.string.account_anonymous),
|
||||||
email: String = stringResource(R.string.account_anonymous_email),
|
email: String = stringResource(R.string.account_anonymous_email),
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
onLogout: () -> Unit = {}
|
onLogout: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = stringResource(R.string.title_account_manager),
|
title = stringResource(R.string.title_account_manager)
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -118,7 +113,7 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_medium)),
|
.padding(vertical = dimensionResource(R.dimen.spacing_small)),
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
AssistHeader()
|
AssistHeader()
|
||||||
@@ -126,7 +121,7 @@ private fun ScreenContent(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
@@ -158,7 +153,9 @@ private fun ScreenContent(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = dimensionResource(R.dimen.height_bottom_adj)),
|
.padding(bottom = dimensionResource(R.dimen.height_bottom_adj)),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.account_logout),
|
text = stringResource(R.string.account_logout),
|
||||||
@@ -171,7 +168,7 @@ private fun ScreenContent(
|
|||||||
painter = painterResource(R.drawable.ic_logout),
|
painter = painterResource(R.drawable.ic_logout),
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_xsmall)))
|
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing_xsmall)))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.action_logout)
|
text = stringResource(R.string.action_logout)
|
||||||
)
|
)
|
||||||
@@ -193,8 +190,8 @@ private fun AssistHeader() {
|
|||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal))
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
items(items = links.keys.toList(), key = { item -> item }) { label ->
|
items(items = links.keys.toList(), key = { item -> item }) { label ->
|
||||||
AssistChip(
|
AssistChip(
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.accounts
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebSettings
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import com.aurora.gplayapi.helpers.AuthHelper
|
||||||
|
import com.aurora.store.AuroraApp
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
|
import com.aurora.store.data.event.AuthEvent
|
||||||
|
import com.aurora.store.data.model.AccountType
|
||||||
|
import com.aurora.store.data.providers.AccountProvider
|
||||||
|
import com.aurora.store.util.AC2DMUtil
|
||||||
|
import com.aurora.store.viewmodel.auth.AuthViewModel
|
||||||
|
|
||||||
|
private const val EMBEDDED_SETUP_URL = "https://accounts.google.com/EmbeddedSetup"
|
||||||
|
private const val AUTH_TOKEN = "oauth_token"
|
||||||
|
private const val JS_PROFILE_EMAIL =
|
||||||
|
"(function() { return document.getElementById('profileIdentifier').innerHTML; })();"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GoogleLoginScreen(
|
||||||
|
onNavigateTo: (Destination) -> Unit,
|
||||||
|
viewModel: AuthViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var progress by remember { mutableFloatStateOf(0f) }
|
||||||
|
var isIndeterminate by remember { mutableStateOf(true) }
|
||||||
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
AuroraApp.events.authEvent.collect { event ->
|
||||||
|
if (event is AuthEvent.GoogleLogin) {
|
||||||
|
if (event.success) {
|
||||||
|
AccountProvider.login(
|
||||||
|
context,
|
||||||
|
event.email,
|
||||||
|
event.token,
|
||||||
|
AuthHelper.Token.AAS,
|
||||||
|
AccountType.GOOGLE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, R.string.toast_aas_token_failed, Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
onNavigateTo(Destination.Splash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (isLoading) {
|
||||||
|
if (isIndeterminate) {
|
||||||
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
} else {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { progress },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
factory = { ctx ->
|
||||||
|
val cookieManager = CookieManager.getInstance()
|
||||||
|
WebView(ctx).apply {
|
||||||
|
cookieManager.removeAllCookies(null)
|
||||||
|
cookieManager.acceptThirdPartyCookies(this)
|
||||||
|
cookieManager.setAcceptThirdPartyCookies(this, true)
|
||||||
|
|
||||||
|
webChromeClient = object : WebChromeClient() {
|
||||||
|
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||||
|
super.onProgressChanged(view, newProgress)
|
||||||
|
if (newProgress != 0) {
|
||||||
|
isIndeterminate = false
|
||||||
|
progress = newProgress / 100f
|
||||||
|
isLoading = newProgress < 100
|
||||||
|
} else {
|
||||||
|
isIndeterminate = true
|
||||||
|
isLoading = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
|
val cookies = CookieManager.getInstance().getCookie(url)
|
||||||
|
if (cookies != null) {
|
||||||
|
val cookieMap = AC2DMUtil.parseCookieString(cookies)
|
||||||
|
if (cookieMap.isNotEmpty() && cookieMap[AUTH_TOKEN] != null) {
|
||||||
|
val oauthToken = cookieMap[AUTH_TOKEN]
|
||||||
|
view.evaluateJavascript(JS_PROFILE_EMAIL) { result ->
|
||||||
|
val email = result.replace("\"", "")
|
||||||
|
viewModel.buildAuthData(view.context, email, oauthToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.apply {
|
||||||
|
allowContentAccess = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
javaScriptEnabled = true
|
||||||
|
cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
safeBrowsingEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUrl(EMBEDDED_SETUP_URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.apps
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material3.PrimaryTabRow
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import com.aurora.gplayapi.helpers.contracts.StreamContract
|
||||||
|
import com.aurora.gplayapi.helpers.contracts.TopChartsContract
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
|
import com.aurora.store.util.Preferences
|
||||||
|
import com.aurora.store.viewmodel.category.CategoryViewModel
|
||||||
|
import com.aurora.store.viewmodel.homestream.StreamViewModel
|
||||||
|
import com.aurora.store.viewmodel.topchart.TopChartViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
internal fun category(pageType: Int): StreamContract.Category =
|
||||||
|
if (pageType == 1) StreamContract.Category.GAME else StreamContract.Category.APPLICATION
|
||||||
|
|
||||||
|
private enum class AppsTab(@StringRes val titleRes: Int) {
|
||||||
|
FOR_YOU(R.string.tab_for_you),
|
||||||
|
TOP_CHARTS(R.string.tab_top_charts),
|
||||||
|
CATEGORIES(R.string.tab_categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppsGamesScreen(
|
||||||
|
pageType: Int,
|
||||||
|
streamViewModel: StreamViewModel = hiltViewModel(key = "stream_$pageType"),
|
||||||
|
topChartViewModel: TopChartViewModel = hiltViewModel(key = "topChart_$pageType"),
|
||||||
|
categoryViewModel: CategoryViewModel = hiltViewModel(key = "category_$pageType"),
|
||||||
|
onNavigateTo: (Destination) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val chartType = if (pageType == 1) {
|
||||||
|
TopChartsContract.Type.GAME
|
||||||
|
} else {
|
||||||
|
TopChartsContract.Type.APPLICATION
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
topChartViewModel.getStreamCluster(chartType, TopChartsContract.Chart.TOP_SELLING_FREE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isForYouEnabled = Preferences.getBoolean(context, Preferences.PREFERENCE_FOR_YOU)
|
||||||
|
val tabs = buildList {
|
||||||
|
if (isForYouEnabled) add(AppsTab.FOR_YOU)
|
||||||
|
add(AppsTab.TOP_CHARTS)
|
||||||
|
add(AppsTab.CATEGORIES)
|
||||||
|
}
|
||||||
|
val pagerState = rememberPagerState { tabs.size }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
PrimaryTabRow(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
selectedTabIndex = pagerState.currentPage
|
||||||
|
) {
|
||||||
|
tabs.forEachIndexed { index, tab ->
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == index,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = { Text(stringResource(tab.titleRes)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalPager(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = pagerState,
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
userScrollEnabled = false
|
||||||
|
) { page ->
|
||||||
|
when (tabs[page]) {
|
||||||
|
AppsTab.FOR_YOU -> ForYouContent(
|
||||||
|
pageType = pageType,
|
||||||
|
viewModel = streamViewModel,
|
||||||
|
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
||||||
|
onHeaderClick = { onNavigateTo(Destination.StreamBrowse(it)) },
|
||||||
|
onClusterScrolled = { cluster ->
|
||||||
|
streamViewModel.observeCluster(category(pageType), cluster)
|
||||||
|
},
|
||||||
|
onScrolledToEnd = {
|
||||||
|
streamViewModel.observe(category(pageType), StreamContract.Type.HOME)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AppsTab.TOP_CHARTS -> TopChartsContent(
|
||||||
|
pageType = pageType,
|
||||||
|
viewModel = topChartViewModel,
|
||||||
|
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) }
|
||||||
|
)
|
||||||
|
|
||||||
|
AppsTab.CATEGORIES -> CategoriesContent(
|
||||||
|
pageType = pageType,
|
||||||
|
viewModel = categoryViewModel,
|
||||||
|
onCategoryClick = { onNavigateTo(Destination.CategoryBrowse(it)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.apps
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import com.aurora.gplayapi.data.models.Category
|
||||||
|
import com.aurora.store.CategoryStash
|
||||||
|
import com.aurora.store.compose.composable.CategoryItem
|
||||||
|
import com.aurora.store.compose.composable.ShimmerCategoryRow
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import com.aurora.store.data.model.ViewState
|
||||||
|
import com.aurora.store.viewmodel.category.CategoryViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun CategoriesContent(
|
||||||
|
pageType: Int,
|
||||||
|
viewModel: CategoryViewModel,
|
||||||
|
onCategoryClick: (Category) -> Unit
|
||||||
|
) {
|
||||||
|
val categoryType = if (pageType == 1) Category.Type.GAME else Category.Type.APPLICATION
|
||||||
|
val state by viewModel.liveData.observeAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(categoryType) {
|
||||||
|
viewModel.getCategoryList(categoryType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val categories = (state as? ViewState.Success<*>)?.data as? CategoryStash
|
||||||
|
val list = categories?.get(categoryType)
|
||||||
|
|
||||||
|
CategoriesBody(list = list, onCategoryClick = onCategoryClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CategoriesBody(list: List<Category>?, onCategoryClick: (Category) -> Unit = {}) {
|
||||||
|
if (list.isNullOrEmpty()) {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(10) { ShimmerCategoryRow() }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(count = list.size, key = { list[it].title }) { index ->
|
||||||
|
CategoryItem(
|
||||||
|
category = list[index],
|
||||||
|
onClick = { onCategoryClick(list[index]) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun CategoriesBodyLoadingPreview() {
|
||||||
|
CategoriesBody(list = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun CategoriesBodyLoadedPreview() {
|
||||||
|
val categories = listOf(
|
||||||
|
"Art & Design",
|
||||||
|
"Auto & Vehicles",
|
||||||
|
"Beauty",
|
||||||
|
"Books & Reference",
|
||||||
|
"Business",
|
||||||
|
"Comics",
|
||||||
|
"Communication",
|
||||||
|
"Dating",
|
||||||
|
"Education",
|
||||||
|
"Entertainment"
|
||||||
|
).map { Category(title = it, imageUrl = "") }
|
||||||
|
CategoriesBody(list = categories)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.apps
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
|
import com.aurora.gplayapi.helpers.contracts.StreamContract
|
||||||
|
import com.aurora.store.HomeStash
|
||||||
|
import com.aurora.store.compose.composable.StreamCarousel
|
||||||
|
import com.aurora.store.compose.composition.observeForced
|
||||||
|
import com.aurora.store.data.model.ViewState
|
||||||
|
import com.aurora.store.viewmodel.homestream.StreamViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ForYouContent(
|
||||||
|
pageType: Int,
|
||||||
|
viewModel: StreamViewModel,
|
||||||
|
onAppClick: (App) -> Unit,
|
||||||
|
onHeaderClick: (StreamCluster) -> Unit,
|
||||||
|
onClusterScrolled: (StreamCluster) -> Unit,
|
||||||
|
onScrolledToEnd: () -> Unit
|
||||||
|
) {
|
||||||
|
val category = category(pageType)
|
||||||
|
val state by viewModel.liveData.observeForced()
|
||||||
|
|
||||||
|
LaunchedEffect(category) {
|
||||||
|
viewModel.getStreamBundle(category, StreamContract.Type.HOME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val streamBundle = (state as? ViewState.Success<*>)?.data as? HomeStash
|
||||||
|
StreamCarousel(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
streamBundle = streamBundle?.get(category),
|
||||||
|
onHeaderClick = onHeaderClick,
|
||||||
|
onAppClick = onAppClick,
|
||||||
|
onClusterScrolled = onClusterScrolled,
|
||||||
|
onScrolledToEnd = onScrolledToEnd
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.apps
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
|
import com.aurora.gplayapi.helpers.contracts.TopChartsContract
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
|
import com.aurora.store.compose.composable.ShimmerAppRow
|
||||||
|
import com.aurora.store.compose.composable.app.LargeAppListItem
|
||||||
|
import com.aurora.store.compose.composition.collectForced
|
||||||
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import com.aurora.store.data.model.ViewState
|
||||||
|
import com.aurora.store.data.model.ViewState.Loading.getDataAs
|
||||||
|
import com.aurora.store.viewmodel.topchart.TopChartViewModel
|
||||||
|
|
||||||
|
private const val LOAD_MORE_THRESHOLD = 2
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TopChartsContent(
|
||||||
|
pageType: Int,
|
||||||
|
viewModel: TopChartViewModel,
|
||||||
|
onAppClick: (App) -> Unit
|
||||||
|
) {
|
||||||
|
val charts = listOf(
|
||||||
|
TopChartsContract.Chart.TOP_SELLING_FREE,
|
||||||
|
TopChartsContract.Chart.TOP_GROSSING,
|
||||||
|
TopChartsContract.Chart.MOVERS_SHAKERS,
|
||||||
|
TopChartsContract.Chart.TOP_SELLING_PAID
|
||||||
|
)
|
||||||
|
val chartTitles = listOf(
|
||||||
|
R.string.tab_top_free,
|
||||||
|
R.string.tab_top_grossing,
|
||||||
|
R.string.tab_trending,
|
||||||
|
R.string.tab_top_paid
|
||||||
|
)
|
||||||
|
val chartType =
|
||||||
|
if (pageType == 1) TopChartsContract.Type.GAME else TopChartsContract.Type.APPLICATION
|
||||||
|
var selectedIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
val selectedChart = charts[selectedIndex]
|
||||||
|
val state by viewModel.state.collectForced(ViewState.Loading)
|
||||||
|
val cluster = state.getDataAs<StreamCluster?>()
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LaunchedEffect(selectedIndex) {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
viewModel.getStreamCluster(chartType, selectedChart)
|
||||||
|
}
|
||||||
|
|
||||||
|
val reachedEnd by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||||
|
val total = listState.layoutInfo.totalItemsCount
|
||||||
|
last >= total - LOAD_MORE_THRESHOLD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(reachedEnd) {
|
||||||
|
if (reachedEnd && cluster?.hasNext() == true) {
|
||||||
|
viewModel.nextCluster(chartType, selectedChart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TopChartsBody(
|
||||||
|
selectedIndex = selectedIndex,
|
||||||
|
chartTitles = chartTitles,
|
||||||
|
state = state,
|
||||||
|
cluster = cluster,
|
||||||
|
listState = listState,
|
||||||
|
onTabSelected = { selectedIndex = it },
|
||||||
|
onRetry = { viewModel.getStreamCluster(chartType, selectedChart) },
|
||||||
|
onAppClick = onAppClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TopChartsBody(
|
||||||
|
selectedIndex: Int,
|
||||||
|
chartTitles: List<Int>,
|
||||||
|
state: ViewState,
|
||||||
|
cluster: StreamCluster?,
|
||||||
|
listState: LazyListState = rememberLazyListState(),
|
||||||
|
onTabSelected: (Int) -> Unit = {},
|
||||||
|
onRetry: () -> Unit = {},
|
||||||
|
onAppClick: (App) -> Unit = {}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
SecondaryScrollableTabRow(
|
||||||
|
selectedTabIndex = selectedIndex,
|
||||||
|
edgePadding = dimensionResource(R.dimen.spacing_small)
|
||||||
|
) {
|
||||||
|
chartTitles.forEachIndexed { index, titleRes ->
|
||||||
|
Tab(
|
||||||
|
selected = selectedIndex == index,
|
||||||
|
onClick = { onTabSelected(index) },
|
||||||
|
text = { Text(stringResource(titleRes)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
state is ViewState.Error -> Placeholder(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
painter = painterResource(R.drawable.ic_apps),
|
||||||
|
message = stringResource(R.string.no_apps_available),
|
||||||
|
actionLabel = stringResource(R.string.action_retry),
|
||||||
|
onAction = onRetry
|
||||||
|
)
|
||||||
|
|
||||||
|
cluster != null && cluster.clusterAppList.isEmpty() -> Placeholder(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
painter = painterResource(R.drawable.ic_apps),
|
||||||
|
message = stringResource(R.string.no_apps_available)
|
||||||
|
)
|
||||||
|
|
||||||
|
cluster != null -> {
|
||||||
|
val apps = cluster.clusterAppList
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
state = listState,
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_small)
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_xsmall)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
items(count = apps.size, key = { apps[it].id }) { index ->
|
||||||
|
LargeAppListItem(
|
||||||
|
app = apps[index],
|
||||||
|
onClick = { onAppClick(apps[index]) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (cluster.hasNext()) {
|
||||||
|
item(key = "progress") {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(dimensionResource(R.dimen.spacing_large)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentPadding = PaddingValues(vertical = dimensionResource(R.dimen.spacing_small)),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_xsmall)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
items(8) { ShimmerAppRow() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val previewChartTitles = listOf(
|
||||||
|
R.string.tab_top_free,
|
||||||
|
R.string.tab_top_grossing,
|
||||||
|
R.string.tab_trending,
|
||||||
|
R.string.tab_top_paid
|
||||||
|
)
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun TopChartsBodyLoadingPreview() {
|
||||||
|
TopChartsBody(
|
||||||
|
selectedIndex = 0,
|
||||||
|
chartTitles = previewChartTitles,
|
||||||
|
state = ViewState.Loading,
|
||||||
|
cluster = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun TopChartsBodyLoadedPreview() {
|
||||||
|
val app = AppPreviewProvider().values.first()
|
||||||
|
val apps = List(5) { i -> app.copy(id = i + 1, packageName = "com.preview.app$i") }
|
||||||
|
val cluster = StreamCluster(clusterAppList = apps)
|
||||||
|
TopChartsBody(
|
||||||
|
selectedIndex = 0,
|
||||||
|
chartTitles = previewChartTitles,
|
||||||
|
state = ViewState.Success(cluster),
|
||||||
|
cluster = cluster
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun TopChartsBodyEmptyPreview() {
|
||||||
|
val cluster = StreamCluster(clusterAppList = emptyList())
|
||||||
|
TopChartsBody(
|
||||||
|
selectedIndex = 1,
|
||||||
|
chartTitles = previewChartTitles,
|
||||||
|
state = ViewState.Success(cluster),
|
||||||
|
cluster = cluster
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun TopChartsBodyErrorPreview() {
|
||||||
|
TopChartsBody(
|
||||||
|
selectedIndex = 0,
|
||||||
|
chartTitles = previewChartTitles,
|
||||||
|
state = ViewState.Error("Network error"),
|
||||||
|
cluster = null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
package com.aurora.store.compose.ui.blacklist
|
package com.aurora.store.compose.ui.blacklist
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -36,8 +38,10 @@ import androidx.compose.material3.rememberSearchBarState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -51,7 +55,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.aurora.Constants
|
import com.aurora.Constants
|
||||||
@@ -64,6 +67,8 @@ import com.aurora.store.compose.composable.TextDividerComposable
|
|||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.compose.ui.blacklist.menu.BlacklistMenu
|
import com.aurora.store.compose.ui.blacklist.menu.BlacklistMenu
|
||||||
import com.aurora.store.compose.ui.blacklist.menu.MenuItem
|
import com.aurora.store.compose.ui.blacklist.menu.MenuItem
|
||||||
|
import com.aurora.store.compose.ui.commons.SortFilterSheet
|
||||||
|
import com.aurora.store.compose.ui.commons.SortFilterState
|
||||||
import com.aurora.store.data.model.BlacklistAppItem
|
import com.aurora.store.data.model.BlacklistAppItem
|
||||||
import com.aurora.store.viewmodel.blacklist.BlacklistViewModel
|
import com.aurora.store.viewmodel.blacklist.BlacklistViewModel
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@@ -72,13 +77,16 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hiltViewModel()) {
|
fun BlacklistScreen(viewModel: BlacklistViewModel = hiltViewModel()) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val packages by viewModel.filteredPackages.collectAsStateWithLifecycle()
|
val packages by viewModel.filteredPackages.collectAsStateWithLifecycle()
|
||||||
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val installers by viewModel.installers.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
packages = packages,
|
packages = packages,
|
||||||
onNavigateUp = onNavigateUp,
|
sortFilterState = state,
|
||||||
|
installers = installers,
|
||||||
isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist },
|
isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist },
|
||||||
onBlacklistImport = { uri ->
|
onBlacklistImport = { uri ->
|
||||||
viewModel.importBlacklist(uri)
|
viewModel.importBlacklist(uri)
|
||||||
@@ -92,14 +100,16 @@ fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hi
|
|||||||
onBlacklistAll = { viewModel.blacklistAll() },
|
onBlacklistAll = { viewModel.blacklistAll() },
|
||||||
onWhitelist = { packageName -> viewModel.whitelist(packageName) },
|
onWhitelist = { packageName -> viewModel.whitelist(packageName) },
|
||||||
onWhitelistAll = { viewModel.whitelistAll() },
|
onWhitelistAll = { viewModel.whitelistAll() },
|
||||||
onSearch = { query -> viewModel.search(query) }
|
onSearch = { query -> viewModel.search(query) },
|
||||||
|
onSortFilterStateChange = viewModel::updateState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
packages: List<BlacklistAppItem>? = null,
|
packages: List<BlacklistAppItem>? = null,
|
||||||
onNavigateUp: () -> Unit = {},
|
sortFilterState: SortFilterState = SortFilterState(),
|
||||||
|
installers: Map<String, String> = emptyMap(),
|
||||||
isPackageBlacklisted: (packageName: String) -> Boolean = { false },
|
isPackageBlacklisted: (packageName: String) -> Boolean = { false },
|
||||||
onBlacklistImport: (uri: Uri) -> Unit = {},
|
onBlacklistImport: (uri: Uri) -> Unit = {},
|
||||||
onBlacklistExport: (uri: Uri) -> Unit = {},
|
onBlacklistExport: (uri: Uri) -> Unit = {},
|
||||||
@@ -107,13 +117,16 @@ private fun ScreenContent(
|
|||||||
onBlacklistAll: () -> Unit = {},
|
onBlacklistAll: () -> Unit = {},
|
||||||
onWhitelist: (packageName: String) -> Unit = {},
|
onWhitelist: (packageName: String) -> Unit = {},
|
||||||
onWhitelistAll: () -> Unit = {},
|
onWhitelistAll: () -> Unit = {},
|
||||||
onSearch: (query: String) -> Unit = {}
|
onSearch: (query: String) -> Unit = {},
|
||||||
|
onSortFilterStateChange: (SortFilterState) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val activity = LocalActivity.current as? ComponentActivity
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val textFieldState = rememberTextFieldState()
|
val textFieldState = rememberTextFieldState()
|
||||||
val searchBarState = rememberSearchBarState()
|
val searchBarState = rememberSearchBarState()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var sheetVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val docImportLauncher = rememberLauncherForActivityResult(
|
val docImportLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.OpenDocument(),
|
contract = ActivityResultContracts.OpenDocument(),
|
||||||
@@ -224,14 +237,22 @@ private fun ScreenContent(
|
|||||||
state = searchBarState,
|
state = searchBarState,
|
||||||
inputField = inputField,
|
inputField = inputField,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateUp) {
|
IconButton(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_arrow_back),
|
painter = painterResource(R.drawable.ic_arrow_back),
|
||||||
contentDescription = stringResource(R.string.action_back)
|
contentDescription = stringResource(R.string.action_back)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = { SetupMenu() },
|
actions = {
|
||||||
|
IconButton(onClick = { sheetVisible = true }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_tune),
|
||||||
|
contentDescription = stringResource(R.string.installed_sort_filter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SetupMenu()
|
||||||
|
},
|
||||||
colors = SearchBarDefaults.appBarWithSearchColors(
|
colors = SearchBarDefaults.appBarWithSearchColors(
|
||||||
appBarContainerColor = Color.Transparent
|
appBarContainerColor = Color.Transparent
|
||||||
)
|
)
|
||||||
@@ -248,8 +269,8 @@ private fun ScreenContent(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onRequestSearch(pkg.displayName) }
|
.clickable { onRequestSearch(pkg.displayName) }
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
vertical = dimensionResource(R.dimen.padding_small)
|
vertical = dimensionResource(R.dimen.spacing_small)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -298,7 +319,7 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = listState,
|
state = listState,
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.margin_xxsmall)
|
dimensionResource(R.dimen.spacing_xsmall)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (selectedPackages.isNotEmpty()) {
|
if (selectedPackages.isNotEmpty()) {
|
||||||
@@ -333,12 +354,20 @@ private fun ScreenContent(
|
|||||||
}
|
}
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sheetVisible) {
|
||||||
|
SortFilterSheet(
|
||||||
|
state = sortFilterState,
|
||||||
|
installers = installers,
|
||||||
|
onStateChange = onSortFilterStateChange,
|
||||||
|
onDismiss = { sheetVisible = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.commons
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import com.aurora.gplayapi.data.models.StreamBundle
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
|
import com.aurora.store.compose.composable.StreamCarousel
|
||||||
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.composition.collectForced
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
|
import com.aurora.store.data.model.ViewState
|
||||||
|
import com.aurora.store.viewmodel.subcategory.CategoryStreamViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CategoryBrowseScreen(
|
||||||
|
title: String,
|
||||||
|
browseUrl: String,
|
||||||
|
onNavigateTo: (Destination) -> Unit,
|
||||||
|
viewModel: CategoryStreamViewModel = hiltViewModel(
|
||||||
|
creationCallback = { factory: CategoryStreamViewModel.Factory ->
|
||||||
|
factory.create(browseUrl)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.viewState.collectForced(ViewState.Loading)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = { TopAppBar(title = title) }
|
||||||
|
) { paddingValues ->
|
||||||
|
if (uiState is ViewState.Error) {
|
||||||
|
Placeholder(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
|
message = stringResource(R.string.error)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val bundle = (uiState as? ViewState.Success<*>)?.data as? StreamBundle
|
||||||
|
StreamCarousel(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
streamBundle = bundle,
|
||||||
|
filterSingleAppClusters = false,
|
||||||
|
onHeaderClick = { cluster ->
|
||||||
|
if (cluster.clusterBrowseUrl.isNotBlank()) {
|
||||||
|
onNavigateTo(Destination.StreamBrowse(cluster))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
||||||
|
onClusterScrolled = { viewModel.fetchNextCluster(it) },
|
||||||
|
onScrolledToEnd = { viewModel.fetchNextPage() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.commons
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||||
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.composable.app.LargeAppListItem
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
|
import com.aurora.store.viewmodel.browse.ExpandedStreamBrowseViewModel
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandedStreamBrowseScreen(
|
||||||
|
browseUrl: String,
|
||||||
|
defaultTitle: String,
|
||||||
|
onNavigateTo: (Destination) -> Unit,
|
||||||
|
viewModel: ExpandedStreamBrowseViewModel = hiltViewModel(
|
||||||
|
creationCallback = { factory: ExpandedStreamBrowseViewModel.Factory ->
|
||||||
|
factory.create(browseUrl)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val title by viewModel.title.collectAsStateWithLifecycle()
|
||||||
|
val apps = viewModel.apps.collectAsLazyPagingItems()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = title.ifBlank { defaultTitle }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
when (apps.loadState.refresh) {
|
||||||
|
is LoadState.Loading -> ContainedLoadingIndicator()
|
||||||
|
|
||||||
|
is LoadState.Error -> {
|
||||||
|
Placeholder(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
|
message = stringResource(R.string.error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
if (apps.itemCount == 0) {
|
||||||
|
Placeholder(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
|
message = stringResource(R.string.no_apps_available)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
count = apps.itemCount,
|
||||||
|
key = { Uuid.random().toString() }
|
||||||
|
) { index ->
|
||||||
|
apps[index]?.let { app ->
|
||||||
|
LargeAppListItem(
|
||||||
|
app = app,
|
||||||
|
onClick = {
|
||||||
|
onNavigateTo(Destination.AppDetails(app.packageName))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.commons
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.NavigationDrawerItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
|
import com.aurora.store.viewmodel.commons.MoreViewModel
|
||||||
|
|
||||||
|
private data class MoreItem(
|
||||||
|
@StringRes val titleRes: Int,
|
||||||
|
@DrawableRes val iconRes: Int,
|
||||||
|
val onClick: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MoreSheet(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onNavigateTo: (Destination) -> Unit = {},
|
||||||
|
viewModel: MoreViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val mainItems = listOf(
|
||||||
|
MoreItem(R.string.title_apps_games, R.drawable.ic_apps) {
|
||||||
|
onNavigateTo(Destination.Installed)
|
||||||
|
},
|
||||||
|
MoreItem(R.string.title_blacklist_manager, R.drawable.ic_blacklist) {
|
||||||
|
onNavigateTo(Destination.Blacklist)
|
||||||
|
},
|
||||||
|
MoreItem(R.string.title_favourites_manager, R.drawable.ic_favorite_unchecked) {
|
||||||
|
onNavigateTo(Destination.Favourite)
|
||||||
|
},
|
||||||
|
MoreItem(R.string.title_spoof_manager, R.drawable.ic_spoof) {
|
||||||
|
onNavigateTo(Destination.Spoof)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val extraItems = listOf(
|
||||||
|
MoreItem(R.string.title_settings, R.drawable.ic_menu_settings) {
|
||||||
|
onNavigateTo(Destination.Settings)
|
||||||
|
},
|
||||||
|
MoreItem(R.string.title_about, R.drawable.ic_menu_about) { onNavigateTo(Destination.About) }
|
||||||
|
)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
) {
|
||||||
|
AccountHeader(
|
||||||
|
viewModel = viewModel,
|
||||||
|
onNavigateToAccounts = {
|
||||||
|
onNavigateTo(Destination.Accounts)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_small),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_small)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
mainItems.forEach { item ->
|
||||||
|
NavigationDrawerItem(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(item.iconRes),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(item.titleRes)) },
|
||||||
|
selected = false,
|
||||||
|
onClick = {
|
||||||
|
item.onClick()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_small),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_small)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
extraItems.forEach { item ->
|
||||||
|
NavigationDrawerItem(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(item.iconRes),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(item.titleRes)) },
|
||||||
|
selected = false,
|
||||||
|
onClick = {
|
||||||
|
item.onClick()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccountHeader(viewModel: MoreViewModel, onNavigateToAccounts: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val isAnonymous = viewModel.authProvider.isAnonymous
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(dimensionResource(R.dimen.spacing_large)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(context)
|
||||||
|
.data(viewModel.authProvider.authData?.userProfile?.artwork?.url)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
placeholder = painterResource(R.drawable.ic_account),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredSize(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = dimensionResource(R.dimen.spacing_medium))
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isAnonymous) {
|
||||||
|
stringResource(R.string.account_anonymous)
|
||||||
|
} else {
|
||||||
|
viewModel.authProvider.authData?.userProfile?.name
|
||||||
|
?: stringResource(R.string.status_unavailable)
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = viewModel.authProvider.authData?.userProfile?.email
|
||||||
|
?: stringResource(R.string.status_unavailable),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onNavigateToAccounts) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_account_manager),
|
||||||
|
contentDescription = stringResource(R.string.manage_account)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.commons
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.aurora.extensions.isQAndAbove
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
|
private const val TAG = "NetworkScreen"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-screen "no network" placeholder shown while [com.aurora.store.data.model.NetworkStatus]
|
||||||
|
* is UNAVAILABLE. Replaces the legacy modal sheet. Auto-dismisses by virtue of its caller
|
||||||
|
* only rendering it while offline.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun NetworkScreen() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Scaffold { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(horizontal = dimensionResource(R.dimen.spacing_xlarge)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_network),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.requiredSize(48.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.title_no_network),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.check_connectivity),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = {
|
||||||
|
try {
|
||||||
|
if (isQAndAbove) {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
|
||||||
|
}
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
Log.i(TAG, "Unable to launch network settings")
|
||||||
|
try {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_check))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun NetworkScreenPreview() {
|
||||||
|
NetworkScreen()
|
||||||
|
}
|
||||||
@@ -35,14 +35,12 @@ import kotlin.random.Random
|
|||||||
@Composable
|
@Composable
|
||||||
fun PermissionRationaleScreen(
|
fun PermissionRationaleScreen(
|
||||||
requiredPermissions: Set<PermissionType> = emptySet(),
|
requiredPermissions: Set<PermissionType> = emptySet(),
|
||||||
onNavigateUp: () -> Unit,
|
|
||||||
onPermissionCallback: (type: PermissionType) -> Unit = {},
|
onPermissionCallback: (type: PermissionType) -> Unit = {},
|
||||||
viewModel: PermissionRationaleViewModel = hiltViewModel()
|
viewModel: PermissionRationaleViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val permissions by viewModel.permissions.collectAsStateWithLifecycle()
|
val permissions by viewModel.permissions.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
permissions = permissions
|
permissions = permissions
|
||||||
.filter { it.type in requiredPermissions }
|
.filter { it.type in requiredPermissions }
|
||||||
.map { permission -> permission.copy(optional = false) },
|
.map { permission -> permission.copy(optional = false) },
|
||||||
@@ -56,7 +54,6 @@ fun PermissionRationaleScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
permissions: List<Permission> = emptyList(),
|
permissions: List<Permission> = emptyList(),
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
onPermissionCallback: (type: PermissionType) -> Unit = {},
|
onPermissionCallback: (type: PermissionType) -> Unit = {},
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
@@ -64,8 +61,7 @@ private fun ScreenContent(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = pluralStringResource(R.plurals.permissions_required, permissions.size),
|
title = pluralStringResource(R.plurals.permissions_required, permissions.size),
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.commons
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.util.Preferences
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-controlled axis for ordering a list of installed apps.
|
||||||
|
*/
|
||||||
|
enum class SortBy {
|
||||||
|
NAME,
|
||||||
|
DATE_INSTALLED,
|
||||||
|
DATE_UPDATED,
|
||||||
|
PACKAGE_NAME,
|
||||||
|
SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SortOrder { ASC, DESC }
|
||||||
|
|
||||||
|
enum class AppType { USER, SYSTEM }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort + filter state shared by screens that list installed apps. Persisted via
|
||||||
|
* [Preferences] using a per-screen [SortFilterPrefKeys] so each screen restores
|
||||||
|
* its own last selection.
|
||||||
|
*
|
||||||
|
* [installer] is the package name of the installing source to filter by, or null
|
||||||
|
* for "all installers". [appTypes] is a non-empty subset; the UI guards against
|
||||||
|
* the empty case by always keeping at least one type selected.
|
||||||
|
*/
|
||||||
|
data class SortFilterState(
|
||||||
|
val sortBy: SortBy = SortBy.NAME,
|
||||||
|
val sortOrder: SortOrder = SortOrder.ASC,
|
||||||
|
val appTypes: Set<AppType> = setOf(AppType.USER),
|
||||||
|
val installer: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SharedPreferences keys a screen uses to persist its [SortFilterState].
|
||||||
|
*/
|
||||||
|
data class SortFilterPrefKeys(
|
||||||
|
val sortBy: String,
|
||||||
|
val sortOrder: String,
|
||||||
|
val appTypes: String,
|
||||||
|
val installer: String
|
||||||
|
)
|
||||||
|
|
||||||
|
fun SortFilterState.save(context: Context, keys: SortFilterPrefKeys) {
|
||||||
|
Preferences.putString(context, keys.sortBy, sortBy.name)
|
||||||
|
Preferences.putString(context, keys.sortOrder, sortOrder.name)
|
||||||
|
Preferences.putStringSet(context, keys.appTypes, appTypes.map { it.name }.toSet())
|
||||||
|
if (installer == null) {
|
||||||
|
Preferences.remove(context, keys.installer)
|
||||||
|
} else {
|
||||||
|
Preferences.putString(context, keys.installer, installer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadSortFilterState(context: Context, keys: SortFilterPrefKeys): SortFilterState {
|
||||||
|
val sortBy = enumValueOrDefault(Preferences.getString(context, keys.sortBy), SortBy.NAME)
|
||||||
|
val sortOrder = enumValueOrDefault(
|
||||||
|
Preferences.getString(context, keys.sortOrder),
|
||||||
|
SortOrder.ASC
|
||||||
|
)
|
||||||
|
val appTypes = Preferences.getStringSet(context, keys.appTypes)
|
||||||
|
.mapNotNull { runCatching { AppType.valueOf(it) }.getOrNull() }
|
||||||
|
.toSet()
|
||||||
|
.ifEmpty { setOf(AppType.USER) }
|
||||||
|
val installer = Preferences.getString(context, keys.installer).ifBlank { null }
|
||||||
|
return SortFilterState(sortBy, sortOrder, appTypes, installer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T : Enum<T>> enumValueOrDefault(name: String, default: T): T =
|
||||||
|
runCatching { enumValueOf<T>(name) }.getOrDefault(default)
|
||||||
|
|
||||||
|
fun SortBy.labelRes(): Int = when (this) {
|
||||||
|
SortBy.NAME -> R.string.installed_sort_name
|
||||||
|
SortBy.DATE_INSTALLED -> R.string.installed_sort_date_installed
|
||||||
|
SortBy.DATE_UPDATED -> R.string.installed_sort_date_updated
|
||||||
|
SortBy.PACKAGE_NAME -> R.string.installed_sort_package_name
|
||||||
|
SortBy.SIZE -> R.string.installed_sort_size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SortOrder.labelRes(): Int = when (this) {
|
||||||
|
SortOrder.ASC -> R.string.installed_sort_ascending
|
||||||
|
SortOrder.DESC -> R.string.installed_sort_descending
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AppType.labelRes(): Int = when (this) {
|
||||||
|
AppType.USER -> R.string.installed_filter_user_apps
|
||||||
|
AppType.SYSTEM -> R.string.installed_filter_system_apps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-app metadata that [applyFilter] and [applySort] read. Each consumer screen
|
||||||
|
* has its own enriched model (e.g. `EnrichedPackage`, `BlacklistAppItem`) that
|
||||||
|
* implements this interface so the shared filter/sort logic stays type-safe.
|
||||||
|
*/
|
||||||
|
interface InstalledAppMeta {
|
||||||
|
val packageName: String
|
||||||
|
val label: String
|
||||||
|
val firstInstallTime: Long
|
||||||
|
val lastUpdateTime: Long
|
||||||
|
val sizeBytes: Long
|
||||||
|
val isSystem: Boolean
|
||||||
|
val installer: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : InstalledAppMeta> List<T>.applyFilter(state: SortFilterState): List<T> {
|
||||||
|
val typeFilter: (T) -> Boolean = when {
|
||||||
|
state.appTypes.containsAll(listOf(AppType.USER, AppType.SYSTEM)) -> { _ -> true }
|
||||||
|
AppType.USER in state.appTypes -> { pkg -> !pkg.isSystem }
|
||||||
|
AppType.SYSTEM in state.appTypes -> { pkg -> pkg.isSystem }
|
||||||
|
else -> { _ -> false }
|
||||||
|
}
|
||||||
|
val installerFilter: (T) -> Boolean = state.installer
|
||||||
|
?.let { wanted -> { pkg: T -> pkg.installer == wanted } }
|
||||||
|
?: { _ -> true }
|
||||||
|
return filter { typeFilter(it) && installerFilter(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : InstalledAppMeta> List<T>.applySort(state: SortFilterState): List<T> {
|
||||||
|
val comparator: Comparator<T> = when (state.sortBy) {
|
||||||
|
SortBy.NAME -> compareBy { it.label.lowercase(Locale.getDefault()) }
|
||||||
|
SortBy.DATE_INSTALLED -> compareBy { it.firstInstallTime }
|
||||||
|
SortBy.DATE_UPDATED -> compareBy { it.lastUpdateTime }
|
||||||
|
SortBy.PACKAGE_NAME -> compareBy { it.packageName.lowercase(Locale.getDefault()) }
|
||||||
|
SortBy.SIZE -> compareBy { it.sizeBytes }
|
||||||
|
}
|
||||||
|
return sortedWith(
|
||||||
|
if (state.sortOrder == SortOrder.ASC) comparator else comparator.reversed()
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.commons
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.SegmentedButton
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sheet that lets the user choose how a list of installed apps is sorted and
|
||||||
|
* filtered. Edits propagate immediately via [onStateChange]; the sheet itself is
|
||||||
|
* stateless.
|
||||||
|
*
|
||||||
|
* @param installers Map of installer package name to human-readable label, used
|
||||||
|
* to populate the installer filter chips. When empty the installer row is
|
||||||
|
* hidden.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SortFilterSheet(
|
||||||
|
state: SortFilterState,
|
||||||
|
installers: Map<String, String>,
|
||||||
|
onStateChange: (SortFilterState) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
SortSection(state = state, onStateChange = onStateChange)
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(vertical = dimensionResource(R.dimen.spacing_xsmall))
|
||||||
|
)
|
||||||
|
FilterSection(
|
||||||
|
state = state,
|
||||||
|
installers = installers,
|
||||||
|
onStateChange = onStateChange
|
||||||
|
)
|
||||||
|
Spacer(Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SortSection(state: SortFilterState, onStateChange: (SortFilterState) -> Unit) {
|
||||||
|
SectionHeader(title = stringResource(R.string.installed_sort_by))
|
||||||
|
SortBy.entries.forEach { option ->
|
||||||
|
SelectableRow(
|
||||||
|
label = stringResource(option.labelRes()),
|
||||||
|
selected = state.sortBy == option,
|
||||||
|
onClick = { onStateChange(state.copy(sortBy = option)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
SortOrder.entries.forEachIndexed { index, order ->
|
||||||
|
SegmentedButton(
|
||||||
|
selected = state.sortOrder == order,
|
||||||
|
onClick = { onStateChange(state.copy(sortOrder = order)) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(
|
||||||
|
index = index,
|
||||||
|
count = SortOrder.entries.size
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(order.labelRes()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun FilterSection(
|
||||||
|
state: SortFilterState,
|
||||||
|
installers: Map<String, String>,
|
||||||
|
onStateChange: (SortFilterState) -> Unit
|
||||||
|
) {
|
||||||
|
SectionHeader(title = stringResource(R.string.installed_filter))
|
||||||
|
AppType.entries.forEach { type ->
|
||||||
|
CheckableRow(
|
||||||
|
label = stringResource(type.labelRes()),
|
||||||
|
checked = type in state.appTypes,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
val next = if (checked) state.appTypes + type else state.appTypes - type
|
||||||
|
// Always keep at least one type selected to avoid an empty list.
|
||||||
|
if (next.isNotEmpty()) onStateChange(state.copy(appTypes = next))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (installers.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.installed_filter_installer),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = state.installer == null,
|
||||||
|
onClick = { onStateChange(state.copy(installer = null)) },
|
||||||
|
label = { Text(text = stringResource(R.string.installed_filter_all)) }
|
||||||
|
)
|
||||||
|
installers.entries.sortedBy { it.value.lowercase() }.forEach { (pkg, label) ->
|
||||||
|
FilterChip(
|
||||||
|
selected = state.installer == pkg,
|
||||||
|
onClick = {
|
||||||
|
onStateChange(
|
||||||
|
state.copy(installer = if (state.installer == pkg) null else pkg)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(text = label) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelectableRow(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
|
) {
|
||||||
|
RadioButton(selected = selected, onClick = onClick)
|
||||||
|
Text(text = label, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CheckableRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onCheckedChange(!checked) }
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
|
) {
|
||||||
|
Checkbox(checked = checked, onCheckedChange = onCheckedChange)
|
||||||
|
Text(text = label, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun SortFilterSheetPreview() {
|
||||||
|
SortFilterSheet(
|
||||||
|
state = SortFilterState(),
|
||||||
|
installers = mapOf(
|
||||||
|
"com.android.vending" to "Google Play Store",
|
||||||
|
"com.aurora.store" to "Aurora Store",
|
||||||
|
"org.fdroid.fdroid" to "F-Droid"
|
||||||
|
),
|
||||||
|
onStateChange = {},
|
||||||
|
onDismiss = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,12 +5,14 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.ui.commons
|
package com.aurora.store.compose.ui.commons
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -26,9 +28,10 @@ import com.aurora.gplayapi.data.models.App
|
|||||||
import com.aurora.gplayapi.data.models.StreamCluster
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||||
import com.aurora.store.compose.composable.Error
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.composable.app.LargeAppListItem
|
import com.aurora.store.compose.composable.app.LargeAppListItem
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.viewmodel.browse.StreamBrowseViewModel
|
import com.aurora.store.viewmodel.browse.StreamBrowseViewModel
|
||||||
@@ -39,8 +42,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
@Composable
|
@Composable
|
||||||
fun StreamBrowseScreen(
|
fun StreamBrowseScreen(
|
||||||
streamCluster: StreamCluster,
|
streamCluster: StreamCluster,
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateTo: (Destination) -> Unit,
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
|
||||||
viewModel: StreamBrowseViewModel = hiltViewModel(
|
viewModel: StreamBrowseViewModel = hiltViewModel(
|
||||||
creationCallback = { factory: StreamBrowseViewModel.Factory ->
|
creationCallback = { factory: StreamBrowseViewModel.Factory ->
|
||||||
factory.create(streamCluster)
|
factory.create(streamCluster)
|
||||||
@@ -52,8 +54,7 @@ fun StreamBrowseScreen(
|
|||||||
ScreenContent(
|
ScreenContent(
|
||||||
title = streamCluster.clusterTitle,
|
title = streamCluster.clusterTitle,
|
||||||
apps = apps,
|
apps = apps,
|
||||||
onNavigateUp = onNavigateUp,
|
onNavigateTo = onNavigateTo
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,19 +62,18 @@ fun StreamBrowseScreen(
|
|||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
title: String = String(),
|
title: String = String(),
|
||||||
apps: LazyPagingItems<App> = emptyPagingItems(),
|
apps: LazyPagingItems<App> = emptyPagingItems(),
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
onNavigateTo: (Destination) -> Unit = {}
|
||||||
onNavigateUp: () -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(title = title, onNavigateUp = onNavigateUp)
|
TopAppBar(title = title)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
when (apps.loadState.refresh) {
|
when (apps.loadState.refresh) {
|
||||||
is LoadState.Loading -> ContainedLoadingIndicator()
|
is LoadState.Loading -> ContainedLoadingIndicator()
|
||||||
|
|
||||||
is LoadState.Error -> {
|
is LoadState.Error -> {
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_disclaimer),
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
message = stringResource(R.string.error)
|
message = stringResource(R.string.error)
|
||||||
@@ -82,7 +82,7 @@ private fun ScreenContent(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (apps.itemCount == 0) {
|
if (apps.itemCount == 0) {
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_disclaimer),
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
message = stringResource(R.string.no_apps_available)
|
message = stringResource(R.string.no_apps_available)
|
||||||
@@ -91,7 +91,10 @@ private fun ScreenContent(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
count = apps.itemCount,
|
count = apps.itemCount,
|
||||||
@@ -100,7 +103,9 @@ private fun ScreenContent(
|
|||||||
apps[index]?.let { app ->
|
apps[index]?.let { app ->
|
||||||
LargeAppListItem(
|
LargeAppListItem(
|
||||||
app = app,
|
app = app,
|
||||||
onClick = { onNavigateToAppDetails(app.packageName) }
|
onClick = {
|
||||||
|
onNavigateTo(Destination.AppDetails(app.packageName))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,11 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
|
||||||
@@ -50,7 +47,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -63,15 +59,21 @@ import com.aurora.extensions.share
|
|||||||
import com.aurora.extensions.toast
|
import com.aurora.extensions.toast
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.data.models.Review
|
import com.aurora.gplayapi.data.models.Review
|
||||||
|
import com.aurora.gplayapi.data.models.StreamBundle
|
||||||
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
|
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
|
||||||
import com.aurora.store.ComposeActivity
|
import com.aurora.store.ComposeActivity
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.ClusterRow
|
||||||
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||||
import com.aurora.store.compose.composable.Error
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.Header
|
|
||||||
import com.aurora.store.compose.composable.ScrollHint
|
import com.aurora.store.compose.composable.ScrollHint
|
||||||
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
|
import com.aurora.store.compose.composable.ShimmerCarouselSection
|
||||||
|
import com.aurora.store.compose.composable.StreamCarousel
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.composable.app.LargeAppListItem
|
import com.aurora.store.compose.composition.collectForced
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.compose.navigation.Screen
|
import com.aurora.store.compose.navigation.Screen
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
@@ -92,6 +94,7 @@ import com.aurora.store.compose.ui.details.menu.AppDetailsMenu
|
|||||||
import com.aurora.store.compose.ui.details.menu.MenuItem
|
import com.aurora.store.compose.ui.details.menu.MenuItem
|
||||||
import com.aurora.store.compose.ui.details.navigation.ExtraScreen
|
import com.aurora.store.compose.ui.details.navigation.ExtraScreen
|
||||||
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
||||||
|
import com.aurora.store.compose.ui.sheets.InstallErrorSheet
|
||||||
import com.aurora.store.data.installer.AppInstaller
|
import com.aurora.store.data.installer.AppInstaller
|
||||||
import com.aurora.store.data.model.AppState
|
import com.aurora.store.data.model.AppState
|
||||||
import com.aurora.store.data.model.PermissionType
|
import com.aurora.store.data.model.PermissionType
|
||||||
@@ -104,14 +107,12 @@ import com.aurora.store.util.PackageUtil
|
|||||||
import com.aurora.store.util.ShortcutManagerUtil
|
import com.aurora.store.util.ShortcutManagerUtil
|
||||||
import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppDetailsScreen(
|
fun AppDetailsScreen(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateTo: (Destination) -> Unit,
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
|
||||||
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||||
forceSinglePane: Boolean = false
|
forceSinglePane: Boolean = false
|
||||||
) {
|
) {
|
||||||
@@ -124,10 +125,22 @@ fun AppDetailsScreen(
|
|||||||
val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle()
|
val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle()
|
||||||
val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle()
|
val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle()
|
||||||
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
|
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
|
||||||
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
|
val installError by viewModel.installError.collectAsStateWithLifecycle()
|
||||||
|
val suggestionsBundle by viewModel.suggestionsBundle.collectForced(initial = null)
|
||||||
|
|
||||||
LaunchedEffect(key1 = packageName) { viewModel.fetchAppDetails(packageName) }
|
LaunchedEffect(key1 = packageName) { viewModel.fetchAppDetails(packageName) }
|
||||||
|
|
||||||
|
app?.let { loadedApp ->
|
||||||
|
installError?.let { err ->
|
||||||
|
InstallErrorSheet(
|
||||||
|
app = loadedApp,
|
||||||
|
error = err.error,
|
||||||
|
extra = err.extra,
|
||||||
|
onDismiss = viewModel::dismissInstallError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = state,
|
targetState = state,
|
||||||
contentKey = { stateKey(it, app) },
|
contentKey = { stateKey(it, app) },
|
||||||
@@ -136,11 +149,10 @@ fun AppDetailsScreen(
|
|||||||
) { currentState ->
|
) { currentState ->
|
||||||
when {
|
when {
|
||||||
currentState is AppState.Loading || app == null ->
|
currentState is AppState.Loading || app == null ->
|
||||||
ScreenContentLoading(onNavigateUp = onNavigateUp)
|
ScreenContentLoading()
|
||||||
|
|
||||||
currentState is AppState.Error ->
|
currentState is AppState.Error ->
|
||||||
ScreenContentError(
|
ScreenContentError(
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
message = currentState.message
|
message = currentState.message
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -149,15 +161,15 @@ fun AppDetailsScreen(
|
|||||||
ScreenContentApp(
|
ScreenContentApp(
|
||||||
app = loadedApp,
|
app = loadedApp,
|
||||||
featuredReviews = featuredReviews,
|
featuredReviews = featuredReviews,
|
||||||
suggestions = suggestions,
|
suggestionsBundle = suggestionsBundle,
|
||||||
isFavorite = favorite,
|
isFavorite = favorite,
|
||||||
isAnonymous = viewModel.authProvider.isAnonymous,
|
isAnonymous = viewModel.authProvider.isAnonymous,
|
||||||
state = currentState,
|
state = currentState,
|
||||||
plexusScores = plexusScores,
|
plexusScores = plexusScores,
|
||||||
dataSafetyReport = dataSafetyReport,
|
dataSafetyReport = dataSafetyReport,
|
||||||
exodusReport = exodusReport,
|
exodusReport = exodusReport,
|
||||||
onNavigateUp = onNavigateUp,
|
onNavigateTo = onNavigateTo,
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails,
|
onLoadMoreCluster = { cluster -> viewModel.loadMoreCluster(cluster) },
|
||||||
onDownload = { requestedApp -> viewModel.enqueueDownload(requestedApp) },
|
onDownload = { requestedApp -> viewModel.enqueueDownload(requestedApp) },
|
||||||
onFavorite = { viewModel.toggleFavourite(loadedApp) },
|
onFavorite = { viewModel.toggleFavourite(loadedApp) },
|
||||||
onCancelDownload = { viewModel.cancelDownload(loadedApp) },
|
onCancelDownload = { viewModel.cancelDownload(loadedApp) },
|
||||||
@@ -196,9 +208,9 @@ private fun stateKey(state: AppState, app: App?): String = when {
|
|||||||
* Composable to show progress while fetching app details
|
* Composable to show progress while fetching app details
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) {
|
private fun ScreenContentLoading() {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { TopAppBar(onNavigateUp = onNavigateUp) }
|
topBar = { TopAppBar() }
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
ContainedLoadingIndicator(modifier = Modifier.padding(paddingValues))
|
ContainedLoadingIndicator(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
@@ -208,11 +220,11 @@ private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) {
|
|||||||
* Composable to display errors related to fetching app details
|
* Composable to display errors related to fetching app details
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContentError(onNavigateUp: () -> Unit = {}, message: String? = null) {
|
private fun ScreenContentError(message: String? = null) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { TopAppBar(onNavigateUp = onNavigateUp) }
|
topBar = { TopAppBar() }
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_apps_outage),
|
painter = painterResource(R.drawable.ic_apps_outage),
|
||||||
message = message ?: stringResource(R.string.toast_app_unavailable)
|
message = message ?: stringResource(R.string.toast_app_unavailable)
|
||||||
@@ -227,15 +239,15 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}, message: String? =
|
|||||||
private fun ScreenContentApp(
|
private fun ScreenContentApp(
|
||||||
app: App,
|
app: App,
|
||||||
featuredReviews: List<Review> = emptyList(),
|
featuredReviews: List<Review> = emptyList(),
|
||||||
suggestions: List<App> = emptyList(),
|
suggestionsBundle: StreamBundle? = StreamBundle.EMPTY,
|
||||||
isFavorite: Boolean = false,
|
isFavorite: Boolean = false,
|
||||||
isAnonymous: Boolean = true,
|
isAnonymous: Boolean = true,
|
||||||
state: AppState = AppState.Unavailable,
|
state: AppState = AppState.Unavailable,
|
||||||
plexusScores: Scores? = null,
|
plexusScores: Scores? = null,
|
||||||
dataSafetyReport: DataSafetyReport? = null,
|
dataSafetyReport: DataSafetyReport? = null,
|
||||||
exodusReport: Report? = null,
|
exodusReport: Report? = null,
|
||||||
onNavigateUp: () -> Unit = {},
|
onNavigateTo: (Destination) -> Unit = {},
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
onLoadMoreCluster: (cluster: StreamCluster) -> Unit = {},
|
||||||
onDownload: (requestedApp: App) -> Unit = {},
|
onDownload: (requestedApp: App) -> Unit = {},
|
||||||
onFavorite: () -> Unit = {},
|
onFavorite: () -> Unit = {},
|
||||||
onCancelDownload: () -> Unit = {},
|
onCancelDownload: () -> Unit = {},
|
||||||
@@ -412,7 +424,6 @@ private fun ScreenContentApp(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
actions = { if (shouldShowMenuOnMainPane) SetupMenu() }
|
actions = { if (shouldShowMenuOnMainPane) SetupMenu() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -427,7 +438,7 @@ private fun ScreenContentApp(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.margin_medium)
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
),
|
),
|
||||||
state = listState
|
state = listState
|
||||||
) {
|
) {
|
||||||
@@ -452,7 +463,7 @@ private fun ScreenContentApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Header(
|
SectionHeader(
|
||||||
title = stringResource(R.string.details_more_about_app),
|
title = stringResource(R.string.details_more_about_app),
|
||||||
subtitle = app.shortDescription,
|
subtitle = app.shortDescription,
|
||||||
onClick = { showExtraPane(ExtraScreen.More) }
|
onClick = { showExtraPane(ExtraScreen.More) }
|
||||||
@@ -488,7 +499,7 @@ private fun ScreenContentApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Header(
|
SectionHeader(
|
||||||
title = stringResource(R.string.details_permission),
|
title = stringResource(R.string.details_permission),
|
||||||
subtitle = if (app.permissions.isNotEmpty()) {
|
subtitle = if (app.permissions.isNotEmpty()) {
|
||||||
stringResource(R.string.permissions_requested, app.permissions.size)
|
stringResource(R.string.permissions_requested, app.permissions.size)
|
||||||
@@ -532,10 +543,17 @@ private fun ScreenContentApp(
|
|||||||
email = app.developerEmail
|
email = app.developerEmail
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldShowMenuOnMainPane) {
|
||||||
|
suggestionClusterItems(
|
||||||
|
suggestionsBundle = suggestionsBundle,
|
||||||
|
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
||||||
|
onClusterScrolled = onLoadMoreCluster
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -546,38 +564,22 @@ private fun ScreenContentApp(
|
|||||||
fun SupportingPane() {
|
fun SupportingPane() {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(actions = { if (!shouldShowMenuOnMainPane) SetupMenu() })
|
TopAppBar(
|
||||||
|
showNavigationIcon = false,
|
||||||
|
actions = { if (!shouldShowMenuOnMainPane) SetupMenu() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
if (suggestions.isNotEmpty()) {
|
val hasContent = suggestionsBundle == null ||
|
||||||
Column(
|
suggestionsBundle.streamClusters.isNotEmpty()
|
||||||
modifier = Modifier
|
if (hasContent) {
|
||||||
.fillMaxSize()
|
StreamCarousel(
|
||||||
.padding(paddingValues)
|
modifier = Modifier.padding(paddingValues),
|
||||||
) {
|
streamBundle = suggestionsBundle,
|
||||||
Row(
|
filterSingleAppClusters = false,
|
||||||
modifier = Modifier.padding(dimensionResource(R.dimen.margin_medium)),
|
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onClusterScrolled = onLoadMoreCluster
|
||||||
) {
|
)
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_suggestions),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Header(title = stringResource(R.string.pref_ui_similar_apps))
|
|
||||||
}
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
|
||||||
) {
|
|
||||||
items(items = suggestions, key = { item -> item.id }) { app ->
|
|
||||||
LargeAppListItem(
|
|
||||||
app = app,
|
|
||||||
onClick = { onNavigateToAppDetails(app.packageName) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -585,52 +587,43 @@ private fun ScreenContentApp(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ExtraPane(screen: NavKey) = when (screen) {
|
fun ExtraPane(screen: NavKey) = when (screen) {
|
||||||
is ExtraScreen.Review -> ReviewScreen(
|
is ExtraScreen.Review -> ReviewScreen(
|
||||||
packageName = app.packageName,
|
packageName = app.packageName
|
||||||
onNavigateUp = ::onNavigateBack
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is ExtraScreen.Exodus -> ExodusScreen(
|
is ExtraScreen.Exodus -> ExodusScreen(
|
||||||
packageName = app.packageName,
|
packageName = app.packageName
|
||||||
onNavigateUp = ::onNavigateBack
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is ExtraScreen.More -> MoreScreen(
|
is ExtraScreen.More -> MoreScreen(
|
||||||
packageName = app.packageName,
|
packageName = app.packageName,
|
||||||
onNavigateUp = ::onNavigateBack,
|
onNavigateTo = onNavigateTo
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is ExtraScreen.Permission -> PermissionScreen(
|
is ExtraScreen.Permission -> PermissionScreen(
|
||||||
packageName = app.packageName,
|
packageName = app.packageName
|
||||||
onNavigateUp = ::onNavigateBack
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is ExtraScreen.Screenshot -> ScreenshotScreen(
|
is ExtraScreen.Screenshot -> ScreenshotScreen(
|
||||||
packageName = app.packageName,
|
packageName = app.packageName,
|
||||||
index = screen.index,
|
index = screen.index
|
||||||
onNavigateUp = ::onNavigateBack
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is ExtraScreen.ManualDownload -> ManualDownloadScreen(
|
is ExtraScreen.ManualDownload -> ManualDownloadScreen(
|
||||||
packageName = app.packageName,
|
packageName = app.packageName,
|
||||||
onNavigateUp = ::onNavigateBack,
|
|
||||||
onRequestInstall = { requestedApp -> onInstall(requestedApp) }
|
onRequestInstall = { requestedApp -> onInstall(requestedApp) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is ExtraScreen.MicroG -> MicroGScreen(
|
is ExtraScreen.MicroG -> MicroGScreen(
|
||||||
packageName = app.packageName,
|
packageName = app.packageName,
|
||||||
onNavigateUp = ::onNavigateBack,
|
|
||||||
onProceed = { onInstall(ignoreMicroG = true) }
|
onProceed = { onInstall(ignoreMicroG = true) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is Screen.DevProfile -> DevProfileScreen(
|
is Screen.DevProfile -> DevProfileScreen(
|
||||||
publisherId = app.developerName,
|
publisherId = app.developerName,
|
||||||
onNavigateUp = ::onNavigateBack,
|
onNavigateTo = onNavigateTo
|
||||||
onNavigateToAppDetails = { onNavigateToAppDetails(it) }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is Screen.PermissionRationale -> PermissionRationaleScreen(
|
is Screen.PermissionRationale -> PermissionRationaleScreen(
|
||||||
onNavigateUp = ::onNavigateBack,
|
|
||||||
requiredPermissions = screen.requiredPermissions,
|
requiredPermissions = screen.requiredPermissions,
|
||||||
onPermissionCallback = { type ->
|
onPermissionCallback = { type ->
|
||||||
val isStoragePermission = type == PermissionType.EXTERNAL_STORAGE ||
|
val isStoragePermission = type == PermissionType.EXTERNAL_STORAGE ||
|
||||||
@@ -658,6 +651,38 @@ private fun ScreenContentApp(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the suggestion stream as cluster rows inside the parent [LazyColumn].
|
||||||
|
* Shows a shimmer placeholder while the bundle is still loading (`null`).
|
||||||
|
*/
|
||||||
|
private fun LazyListScope.suggestionClusterItems(
|
||||||
|
suggestionsBundle: StreamBundle?,
|
||||||
|
onAppClick: (App) -> Unit,
|
||||||
|
onClusterScrolled: (StreamCluster) -> Unit
|
||||||
|
) {
|
||||||
|
if (suggestionsBundle == null) {
|
||||||
|
item(key = "suggestions-shimmer") { ShimmerCarouselSection() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val clusters = suggestionsBundle.streamClusters.values.filter {
|
||||||
|
it.clusterTitle.isNotBlank() && it.clusterAppList.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
clusters.forEach { cluster ->
|
||||||
|
item(key = "cluster-header-${cluster.id}") {
|
||||||
|
SectionHeader(title = cluster.clusterTitle)
|
||||||
|
}
|
||||||
|
item(key = "cluster-row-${cluster.id}") {
|
||||||
|
ClusterRow(
|
||||||
|
cluster = cluster,
|
||||||
|
onAppClick = onAppClick,
|
||||||
|
onClusterScrolled = onClusterScrolled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
@PreviewScreenSizes
|
@PreviewScreenSizes
|
||||||
@Composable
|
@Composable
|
||||||
@@ -665,7 +690,20 @@ private fun AppDetailsScreenPreview(@PreviewParameter(AppPreviewProvider::class)
|
|||||||
ScreenContentApp(
|
ScreenContentApp(
|
||||||
app = app,
|
app = app,
|
||||||
isAnonymous = false,
|
isAnonymous = false,
|
||||||
suggestions = List(10) { app.copy(id = Random.nextInt()) }
|
suggestionsBundle = StreamBundle(
|
||||||
|
streamClusters = mapOf(
|
||||||
|
1 to StreamCluster(
|
||||||
|
id = 1,
|
||||||
|
clusterTitle = "Similar apps",
|
||||||
|
clusterAppList = List(8) { app.copy(id = it) }
|
||||||
|
),
|
||||||
|
2 to StreamCluster(
|
||||||
|
id = 2,
|
||||||
|
clusterTitle = "More by ${app.developerName}",
|
||||||
|
clusterAppList = List(5) { app.copy(id = 100 + it) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.aurora.Constants.EXODUS_REPORT_URL
|
import com.aurora.Constants.EXODUS_REPORT_URL
|
||||||
@@ -41,9 +40,9 @@ import com.aurora.extensions.browse
|
|||||||
import com.aurora.extensions.isWindowCompact
|
import com.aurora.extensions.isWindowCompact
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Error
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.Header
|
|
||||||
import com.aurora.store.compose.composable.ScrollHint
|
import com.aurora.store.compose.composable.ScrollHint
|
||||||
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.composable.details.ExodusListItem
|
import com.aurora.store.compose.composable.details.ExodusListItem
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
@@ -56,7 +55,6 @@ import com.aurora.store.viewmodel.details.ExodusViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun ExodusScreen(
|
fun ExodusScreen(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
onNavigateUp: () -> Unit,
|
|
||||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||||
exodusViewModel: ExodusViewModel = hiltViewModel(key = "$packageName/exodus"),
|
exodusViewModel: ExodusViewModel = hiltViewModel(key = "$packageName/exodus"),
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
@@ -80,7 +78,6 @@ fun ExodusScreen(
|
|||||||
null -> {
|
null -> {
|
||||||
ScreenContentError(
|
ScreenContentError(
|
||||||
topAppBarTitle = topAppBarTitle,
|
topAppBarTitle = topAppBarTitle,
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") }
|
onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -90,7 +87,6 @@ fun ExodusScreen(
|
|||||||
topAppBarTitle = topAppBarTitle,
|
topAppBarTitle = topAppBarTitle,
|
||||||
report = report,
|
report = report,
|
||||||
trackers = trackers,
|
trackers = trackers,
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") }
|
onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -102,7 +98,6 @@ private fun ScreenContentReport(
|
|||||||
topAppBarTitle: String? = null,
|
topAppBarTitle: String? = null,
|
||||||
report: Report? = null,
|
report: Report? = null,
|
||||||
trackers: List<ExodusTracker> = emptyList(),
|
trackers: List<ExodusTracker> = emptyList(),
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
onRequestAnalysis: () -> Unit = {},
|
onRequestAnalysis: () -> Unit = {},
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
@@ -112,8 +107,7 @@ private fun ScreenContentReport(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = topAppBarTitle,
|
title = topAppBarTitle,
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -134,12 +128,12 @@ private fun ScreenContentReport(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
state = listState
|
state = listState
|
||||||
) {
|
) {
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Surface(modifier = Modifier.fillMaxWidth()) {
|
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||||
Header(
|
SectionHeader(
|
||||||
title = if (report?.trackers.isNullOrEmpty()) {
|
title = if (report?.trackers.isNullOrEmpty()) {
|
||||||
stringResource(R.string.exodus_no_tracker)
|
stringResource(R.string.exodus_no_tracker)
|
||||||
} else {
|
} else {
|
||||||
@@ -161,7 +155,6 @@ private fun ScreenContentReport(
|
|||||||
}
|
}
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,7 +167,6 @@ private fun ScreenContentReport(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContentError(
|
private fun ScreenContentError(
|
||||||
topAppBarTitle: String? = null,
|
topAppBarTitle: String? = null,
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
onRequestAnalysis: () -> Unit = {},
|
onRequestAnalysis: () -> Unit = {},
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
@@ -182,16 +174,15 @@ private fun ScreenContentError(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = topAppBarTitle,
|
title = topAppBarTitle,
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_disclaimer),
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
message = stringResource(R.string.failed_to_fetch_report),
|
message = stringResource(R.string.failed_to_fetch_report),
|
||||||
actionMessage = stringResource(R.string.action_request_analysis),
|
actionLabel = stringResource(R.string.action_request_analysis),
|
||||||
onAction = onRequestAnalysis
|
onAction = onRequestAnalysis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.ui.details
|
package com.aurora.store.compose.ui.details
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -69,7 +71,6 @@ import kotlinx.coroutines.launch
|
|||||||
@Composable
|
@Composable
|
||||||
fun ManualDownloadScreen(
|
fun ManualDownloadScreen(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
onNavigateUp: () -> Unit,
|
|
||||||
onRequestInstall: (requestedApp: App) -> Unit,
|
onRequestInstall: (requestedApp: App) -> Unit,
|
||||||
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
@@ -85,7 +86,6 @@ fun ManualDownloadScreen(
|
|||||||
state = state,
|
state = state,
|
||||||
topAppBarTitle = topAppBarTitle,
|
topAppBarTitle = topAppBarTitle,
|
||||||
currentVersionCode = app!!.versionCode,
|
currentVersionCode = app!!.versionCode,
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
onRequestInstall = { versionCode ->
|
onRequestInstall = { versionCode ->
|
||||||
val requestedApp = app!!.copy(
|
val requestedApp = app!!.copy(
|
||||||
versionCode = versionCode,
|
versionCode = versionCode,
|
||||||
@@ -105,10 +105,10 @@ private fun ScreenContent(
|
|||||||
state: AppState = AppState.Unavailable,
|
state: AppState = AppState.Unavailable,
|
||||||
topAppBarTitle: String? = null,
|
topAppBarTitle: String? = null,
|
||||||
currentVersionCode: Long = 0L,
|
currentVersionCode: Long = 0L,
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
onRequestInstall: (versionCode: Long) -> Unit = {},
|
onRequestInstall: (versionCode: Long) -> Unit = {},
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
|
val activity = LocalActivity.current as? ComponentActivity
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
val errorMessage = stringResource(R.string.manual_download_version_error)
|
val errorMessage = stringResource(R.string.manual_download_version_error)
|
||||||
@@ -130,8 +130,7 @@ private fun ScreenContent(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = topAppBarTitle,
|
title = topAppBarTitle,
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackBarHostState) }
|
snackbarHost = { SnackbarHost(hostState = snackBarHostState) }
|
||||||
@@ -140,12 +139,14 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Info(
|
Info(
|
||||||
painter = painterResource(R.drawable.ic_download_manager),
|
painter = painterResource(R.drawable.ic_download_manager),
|
||||||
@@ -182,12 +183,12 @@ private fun ScreenContent(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.padding_medium)
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
modifier = Modifier.weight(1F),
|
modifier = Modifier.weight(1F),
|
||||||
onClick = onNavigateUp
|
onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.action_close),
|
text = stringResource(R.string.action_close),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.ui.details
|
package com.aurora.store.compose.ui.details
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -55,7 +57,6 @@ import com.aurora.store.viewmodel.onboarding.MicroGViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun MicroGScreen(
|
fun MicroGScreen(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
onNavigateUp: () -> Unit,
|
|
||||||
onProceed: () -> Unit,
|
onProceed: () -> Unit,
|
||||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||||
viewModel: MicroGViewModel = hiltViewModel(),
|
viewModel: MicroGViewModel = hiltViewModel(),
|
||||||
@@ -70,7 +71,6 @@ fun MicroGScreen(
|
|||||||
ScreenContent(
|
ScreenContent(
|
||||||
topAppBarTitle = topAppBarTitle,
|
topAppBarTitle = topAppBarTitle,
|
||||||
uiState = viewModel.uiState,
|
uiState = viewModel.uiState,
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
onInstall = { viewModel.downloadMicroG() },
|
onInstall = { viewModel.downloadMicroG() },
|
||||||
onRetry = { viewModel.retryDownload() },
|
onRetry = { viewModel.retryDownload() },
|
||||||
onProceed = onProceed
|
onProceed = onProceed
|
||||||
@@ -81,12 +81,12 @@ fun MicroGScreen(
|
|||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
topAppBarTitle: String? = null,
|
topAppBarTitle: String? = null,
|
||||||
uiState: MicroGUIState = MicroGUIState(),
|
uiState: MicroGUIState = MicroGUIState(),
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
onInstall: () -> Unit = {},
|
onInstall: () -> Unit = {},
|
||||||
onRetry: () -> Unit = {},
|
onRetry: () -> Unit = {},
|
||||||
onProceed: () -> Unit = {},
|
onProceed: () -> Unit = {},
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
|
val activity = LocalActivity.current as? ComponentActivity
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
@@ -94,8 +94,7 @@ private fun ScreenContent(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = topAppBarTitle,
|
title = topAppBarTitle,
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
@@ -104,15 +103,15 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium))
|
.padding(horizontal = dimensionResource(R.dimen.spacing_medium))
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_small)),
|
.padding(vertical = dimensionResource(R.dimen.spacing_small)),
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.padding_medium)
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
modifier = Modifier.weight(1F),
|
modifier = Modifier.weight(1F),
|
||||||
onClick = onNavigateUp
|
onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.action_cancel),
|
text = stringResource(R.string.action_cancel),
|
||||||
@@ -148,7 +147,7 @@ private fun ScreenContent(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
state = listState
|
state = listState
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
@@ -174,7 +173,6 @@ private fun ScreenContent(
|
|||||||
|
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,18 +32,18 @@ import androidx.compose.ui.text.fromHtml
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.aurora.extensions.adaptiveNavigationIcon
|
import com.aurora.extensions.adaptiveNavigationIcon
|
||||||
import com.aurora.extensions.isWindowCompact
|
import com.aurora.extensions.isWindowCompact
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Header
|
|
||||||
import com.aurora.store.compose.composable.Info
|
import com.aurora.store.compose.composable.Info
|
||||||
import com.aurora.store.compose.composable.ScrollHint
|
import com.aurora.store.compose.composable.ScrollHint
|
||||||
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.composable.app.AppListItem
|
import com.aurora.store.compose.composable.app.AppListItem
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
||||||
@@ -52,8 +52,7 @@ import com.aurora.store.viewmodel.details.MoreViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun MoreScreen(
|
fun MoreScreen(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateTo: (Destination) -> Unit,
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
|
||||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||||
moreViewModel: MoreViewModel = hiltViewModel(
|
moreViewModel: MoreViewModel = hiltViewModel(
|
||||||
key = "$packageName/more",
|
key = "$packageName/more",
|
||||||
@@ -68,8 +67,7 @@ fun MoreScreen(
|
|||||||
ScreenContent(
|
ScreenContent(
|
||||||
app = app!!,
|
app = app!!,
|
||||||
dependencies = dependencies,
|
dependencies = dependencies,
|
||||||
onNavigateUp = onNavigateUp,
|
onNavigateTo = onNavigateTo
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +75,7 @@ fun MoreScreen(
|
|||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
app: App,
|
app: App,
|
||||||
dependencies: List<App>? = null,
|
dependencies: List<App>? = null,
|
||||||
onNavigateUp: () -> Unit = {},
|
onNavigateTo: (Destination) -> Unit = {},
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
val topAppBarTitle = when {
|
val topAppBarTitle = when {
|
||||||
@@ -90,8 +87,7 @@ private fun ScreenContent(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = topAppBarTitle,
|
title = topAppBarTitle,
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -104,16 +100,18 @@ private fun ScreenContent(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = listState,
|
state = listState,
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Header(title = stringResource(R.string.details_description))
|
SectionHeader(title = stringResource(R.string.details_description))
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium)
|
horizontal = dimensionResource(R.dimen.spacing_medium)
|
||||||
),
|
),
|
||||||
text = AnnotatedString.fromHtml(
|
text = AnnotatedString.fromHtml(
|
||||||
htmlString = app.description
|
htmlString = app.description
|
||||||
@@ -126,7 +124,7 @@ private fun ScreenContent(
|
|||||||
if (dependencies != null) {
|
if (dependencies != null) {
|
||||||
AppDependencies(
|
AppDependencies(
|
||||||
dependencies = dependencies,
|
dependencies = dependencies,
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails
|
onNavigateTo = onNavigateTo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +135,6 @@ private fun ScreenContent(
|
|||||||
}
|
}
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -148,11 +145,8 @@ private fun ScreenContent(
|
|||||||
* Composable to show dependencies of an app
|
* Composable to show dependencies of an app
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppDependencies(
|
private fun AppDependencies(dependencies: List<App>, onNavigateTo: (Destination) -> Unit) {
|
||||||
dependencies: List<App>,
|
SectionHeader(title = stringResource(R.string.details_dependencies))
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit
|
|
||||||
) {
|
|
||||||
Header(title = stringResource(R.string.details_dependencies))
|
|
||||||
if (dependencies.isEmpty()) {
|
if (dependencies.isEmpty()) {
|
||||||
Info(
|
Info(
|
||||||
title = AnnotatedString(text = stringResource(R.string.details_no_dependencies))
|
title = AnnotatedString(text = stringResource(R.string.details_no_dependencies))
|
||||||
@@ -162,7 +156,7 @@ private fun AppDependencies(
|
|||||||
items(items = dependencies, key = { item -> item.id }) { app ->
|
items(items = dependencies, key = { item -> item.id }) { app ->
|
||||||
AppListItem(
|
AppListItem(
|
||||||
app = app,
|
app = app,
|
||||||
onClick = { onNavigateToAppDetails(app.packageName) }
|
onClick = { onNavigateTo(Destination.AppDetails(app.packageName)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +168,7 @@ private fun AppDependencies(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppInfoMore(app: App) {
|
private fun AppInfoMore(app: App) {
|
||||||
Header(title = stringResource(R.string.details_more_info))
|
SectionHeader(title = stringResource(R.string.details_more_info))
|
||||||
Info(
|
Info(
|
||||||
title = AnnotatedString(
|
title = AnnotatedString(
|
||||||
text = stringResource(R.string.details_more_package_name)
|
text = stringResource(R.string.details_more_package_name)
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.aurora.extensions.adaptiveNavigationIcon
|
import com.aurora.extensions.adaptiveNavigationIcon
|
||||||
@@ -47,7 +46,6 @@ import com.aurora.store.viewmodel.details.PermissionViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun PermissionScreen(
|
fun PermissionScreen(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
onNavigateUp: () -> Unit,
|
|
||||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||||
permissionViewModel: PermissionViewModel = hiltViewModel(
|
permissionViewModel: PermissionViewModel = hiltViewModel(
|
||||||
key = "$packageName/permission",
|
key = "$packageName/permission",
|
||||||
@@ -67,7 +65,6 @@ fun PermissionScreen(
|
|||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
topAppBarTitle = topAppBarTitle,
|
topAppBarTitle = topAppBarTitle,
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
permissionsInfo = permissionsInfo
|
permissionsInfo = permissionsInfo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -76,7 +73,6 @@ fun PermissionScreen(
|
|||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
topAppBarTitle: String? = null,
|
topAppBarTitle: String? = null,
|
||||||
permissionsInfo: Map<String, PermissionInfo> = emptyMap(),
|
permissionsInfo: Map<String, PermissionInfo> = emptyMap(),
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
val packageManager = LocalContext.current.packageManager
|
val packageManager = LocalContext.current.packageManager
|
||||||
@@ -85,8 +81,7 @@ private fun ScreenContent(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = topAppBarTitle,
|
title = topAppBarTitle,
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -99,9 +94,11 @@ private fun ScreenContent(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
state = listState,
|
state = listState,
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
items(items = permissionsInfo.keys.toList(), key = { it }) { permission ->
|
items(items = permissionsInfo.keys.toList(), key = { it }) { permission ->
|
||||||
val permissionInfo = permissionsInfo.getValue(permission)
|
val permissionInfo = permissionsInfo.getValue(permission)
|
||||||
@@ -127,7 +124,6 @@ private fun ScreenContent(
|
|||||||
}
|
}
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
@@ -50,7 +49,7 @@ import com.aurora.extensions.isWindowCompact
|
|||||||
import com.aurora.gplayapi.data.models.Review
|
import com.aurora.gplayapi.data.models.Review
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||||
import com.aurora.store.compose.composable.Error
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.ScrollHint
|
import com.aurora.store.compose.composable.ScrollHint
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.composable.details.ReviewListItem
|
import com.aurora.store.compose.composable.details.ReviewListItem
|
||||||
@@ -64,7 +63,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
@Composable
|
@Composable
|
||||||
fun ReviewScreen(
|
fun ReviewScreen(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
onNavigateUp: () -> Unit,
|
|
||||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||||
reviewViewModel: ReviewViewModel = hiltViewModel(
|
reviewViewModel: ReviewViewModel = hiltViewModel(
|
||||||
key = "$packageName/review",
|
key = "$packageName/review",
|
||||||
@@ -85,7 +83,6 @@ fun ReviewScreen(
|
|||||||
ScreenContent(
|
ScreenContent(
|
||||||
topAppBarTitle = topAppBarTitle,
|
topAppBarTitle = topAppBarTitle,
|
||||||
reviews = reviews,
|
reviews = reviews,
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
onFilter = { filter -> reviewViewModel.fetchReviews(filter) }
|
onFilter = { filter -> reviewViewModel.fetchReviews(filter) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -93,7 +90,6 @@ fun ReviewScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
topAppBarTitle: String? = null,
|
topAppBarTitle: String? = null,
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
reviews: LazyPagingItems<Review> = emptyPagingItems(),
|
reviews: LazyPagingItems<Review> = emptyPagingItems(),
|
||||||
onFilter: (filter: Review.Filter) -> Unit = {},
|
onFilter: (filter: Review.Filter) -> Unit = {},
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
@@ -103,8 +99,7 @@ private fun ScreenContent(
|
|||||||
Column {
|
Column {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = topAppBarTitle,
|
title = topAppBarTitle,
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
FilterHeader { filter -> onFilter(filter) }
|
FilterHeader { filter -> onFilter(filter) }
|
||||||
}
|
}
|
||||||
@@ -119,7 +114,7 @@ private fun ScreenContent(
|
|||||||
is LoadState.Loading -> ContainedLoadingIndicator()
|
is LoadState.Loading -> ContainedLoadingIndicator()
|
||||||
|
|
||||||
is LoadState.Error -> {
|
is LoadState.Error -> {
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_disclaimer),
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
message = stringResource(R.string.error)
|
message = stringResource(R.string.error)
|
||||||
@@ -142,7 +137,6 @@ private fun ScreenContent(
|
|||||||
}
|
}
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -173,8 +167,8 @@ private fun FilterHeader(onClick: (filter: Review.Filter) -> Unit) {
|
|||||||
|
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.margin_normal)),
|
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal))
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
items(items = filters.keys.toList(), key = { item -> item }) { filter ->
|
items(items = filters.keys.toList(), key = { item -> item }) { filter ->
|
||||||
val selected = activeFilter == filter
|
val selected = activeFilter == filter
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
@@ -36,11 +36,11 @@ import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
|||||||
fun ScreenshotScreen(
|
fun ScreenshotScreen(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
index: Int,
|
index: Int,
|
||||||
onNavigateUp: () -> Unit,
|
|
||||||
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
val app by viewModel.app.collectAsStateWithLifecycle()
|
val app by viewModel.app.collectAsStateWithLifecycle()
|
||||||
|
val screenshots = app?.screenshots ?: emptyList()
|
||||||
|
|
||||||
val topAppBarTitle = when {
|
val topAppBarTitle = when {
|
||||||
windowAdaptiveInfo.isWindowCompact -> app!!.displayName
|
windowAdaptiveInfo.isWindowCompact -> app!!.displayName
|
||||||
@@ -49,8 +49,7 @@ fun ScreenshotScreen(
|
|||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
topAppBarTitle = topAppBarTitle,
|
topAppBarTitle = topAppBarTitle,
|
||||||
onNavigateUp = onNavigateUp,
|
screenshots = screenshots.distinctBy { it.url },
|
||||||
screenshots = app!!.screenshots,
|
|
||||||
index = index
|
index = index
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -58,12 +57,11 @@ fun ScreenshotScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
topAppBarTitle: String? = null,
|
topAppBarTitle: String? = null,
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
screenshots: List<Artwork> = emptyList(),
|
screenshots: List<Artwork> = emptyList(),
|
||||||
index: Int = 0,
|
index: Int = 0,
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
val displayMetrics = LocalContext.current.resources.displayMetrics
|
val displayMetrics = LocalResources.current.displayMetrics
|
||||||
val pagerState = rememberPagerState(initialPage = index) { screenshots.size }
|
val pagerState = rememberPagerState(initialPage = index) { screenshots.size }
|
||||||
|
|
||||||
LaunchedEffect(key1 = index) {
|
LaunchedEffect(key1 = index) {
|
||||||
@@ -74,8 +72,7 @@ private fun ScreenContent(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = topAppBarTitle,
|
title = topAppBarTitle,
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -83,7 +80,8 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
state = pagerState
|
state = pagerState,
|
||||||
|
key = { screenshots[it].url }
|
||||||
) { page ->
|
) { page ->
|
||||||
val artwork = screenshots[page]
|
val artwork = screenshots[page]
|
||||||
ScreenshotListItem(
|
ScreenshotListItem(
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ fun Actions(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium))),
|
.padding(PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium))),
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
val buttonWidthModifier = when {
|
val buttonWidthModifier = when {
|
||||||
windowAdaptiveInfo.isWindowCompact -> Modifier.weight(1F)
|
windowAdaptiveInfo.isWindowCompact -> Modifier.weight(1F)
|
||||||
@@ -92,7 +92,7 @@ fun Actions(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ActionsPreview() {
|
private fun ActionsPreview() {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Actions(
|
Actions(
|
||||||
primaryActionDisplayName = stringResource(R.string.action_install),
|
primaryActionDisplayName = stringResource(R.string.action_install),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Header
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
@@ -38,11 +38,11 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Changelog(changelog: String) {
|
fun Changelog(changelog: String) {
|
||||||
Header(title = stringResource(R.string.details_changelog))
|
SectionHeader(title = stringResource(R.string.details_changelog))
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(dimensionResource(R.dimen.padding_medium))
|
.padding(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -50,8 +50,8 @@ fun Changelog(changelog: String) {
|
|||||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
||||||
.background(color = MaterialTheme.colorScheme.secondaryContainer)
|
.background(color = MaterialTheme.colorScheme.secondaryContainer)
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||||
vertical = dimensionResource(R.dimen.padding_small)
|
vertical = dimensionResource(R.dimen.spacing_small)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -71,7 +71,7 @@ fun Changelog(changelog: String) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ChangelogPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
private fun ChangelogPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Changelog(changelog = app.changes)
|
Changelog(changelog = app.changes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Header
|
|
||||||
import com.aurora.store.compose.composable.Info
|
import com.aurora.store.compose.composable.Info
|
||||||
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.compose.theme.successColor
|
import com.aurora.store.compose.theme.successColor
|
||||||
import com.aurora.store.compose.theme.warningColor
|
import com.aurora.store.compose.theme.warningColor
|
||||||
@@ -30,7 +30,7 @@ import com.aurora.store.data.model.Scores
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Compatibility(needsGms: Boolean, plexusScores: Scores? = null) {
|
fun Compatibility(needsGms: Boolean, plexusScores: Scores? = null) {
|
||||||
Header(
|
SectionHeader(
|
||||||
title = stringResource(R.string.details_compatibility_title),
|
title = stringResource(R.string.details_compatibility_title),
|
||||||
subtitle = stringResource(R.string.plexus_powered)
|
subtitle = stringResource(R.string.plexus_powered)
|
||||||
)
|
)
|
||||||
@@ -82,7 +82,7 @@ fun Compatibility(needsGms: Boolean, plexusScores: Scores? = null) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun CompatibilityPreview() {
|
private fun CompatibilityPreview() {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Compatibility(needsGms = true)
|
Compatibility(needsGms = true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import com.aurora.gplayapi.data.models.datasafety.Entry
|
|||||||
import com.aurora.gplayapi.data.models.datasafety.EntryType
|
import com.aurora.gplayapi.data.models.datasafety.EntryType
|
||||||
import com.aurora.gplayapi.data.models.datasafety.Report
|
import com.aurora.gplayapi.data.models.datasafety.Report
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Header
|
|
||||||
import com.aurora.store.compose.composable.Info
|
import com.aurora.store.compose.composable.Info
|
||||||
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
fun DataSafety(report: Report, privacyPolicyUrl: String) {
|
fun DataSafety(report: Report, privacyPolicyUrl: String) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
Header(
|
SectionHeader(
|
||||||
title = stringResource(R.string.details_data_safety_title),
|
title = stringResource(R.string.details_data_safety_title),
|
||||||
subtitle = stringResource(R.string.details_data_safety_subtitle),
|
subtitle = stringResource(R.string.details_data_safety_subtitle),
|
||||||
onClick = { context.browse(privacyPolicyUrl) }
|
onClick = { context.browse(privacyPolicyUrl) }
|
||||||
@@ -86,7 +86,7 @@ fun DataSafety(report: Report, privacyPolicyUrl: String) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DataSafetyPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
private fun DataSafetyPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
DataSafety(
|
DataSafety(
|
||||||
privacyPolicyUrl = app.privacyPolicyUrl,
|
privacyPolicyUrl = app.privacyPolicyUrl,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ fun Details(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(dimensionResource(R.dimen.margin_medium))
|
.padding(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
AnimatedAppIcon(
|
AnimatedAppIcon(
|
||||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
|
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
|
||||||
@@ -117,7 +117,7 @@ fun Details(
|
|||||||
inProgress = state.inProgress(),
|
inProgress = state.inProgress(),
|
||||||
progress = state.progress()
|
progress = state.progress()
|
||||||
)
|
)
|
||||||
Column(modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))) {
|
Column(modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_small))) {
|
||||||
Text(
|
Text(
|
||||||
text = app.displayName,
|
text = app.displayName,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
@@ -172,7 +172,7 @@ fun Details(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DetailsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
private fun DetailsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Details(app = app)
|
Details(app = app)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import com.aurora.extensions.copyToClipBoard
|
|||||||
import com.aurora.extensions.mailTo
|
import com.aurora.extensions.mailTo
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Header
|
|
||||||
import com.aurora.store.compose.composable.Info
|
import com.aurora.store.compose.composable.Info
|
||||||
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
fun DeveloperDetails(address: String, website: String, email: String) {
|
fun DeveloperDetails(address: String, website: String, email: String) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
Header(title = stringResource(R.string.details_dev_details))
|
SectionHeader(title = stringResource(R.string.details_dev_details))
|
||||||
Column {
|
Column {
|
||||||
if (website.isNotBlank()) {
|
if (website.isNotBlank()) {
|
||||||
Info(
|
Info(
|
||||||
@@ -74,7 +74,7 @@ fun DeveloperDetails(address: String, website: String, email: String) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DeveloperDetailsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
private fun DeveloperDetailsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
DeveloperDetails(
|
DeveloperDetails(
|
||||||
address = app.developerAddress,
|
address = app.developerAddress,
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Header
|
|
||||||
import com.aurora.store.compose.composable.Info
|
import com.aurora.store.compose.composable.Info
|
||||||
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.compose.theme.successColor
|
import com.aurora.store.compose.theme.successColor
|
||||||
import com.aurora.store.compose.theme.warningColor
|
import com.aurora.store.compose.theme.warningColor
|
||||||
@@ -30,7 +30,7 @@ import com.aurora.store.data.model.Report
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Privacy(report: Report?, onNavigateToDetailsExodus: (() -> Unit)? = null) {
|
fun Privacy(report: Report?, onNavigateToDetailsExodus: (() -> Unit)? = null) {
|
||||||
Header(
|
SectionHeader(
|
||||||
title = stringResource(R.string.details_privacy),
|
title = stringResource(R.string.details_privacy),
|
||||||
subtitle = stringResource(R.string.exodus_powered),
|
subtitle = stringResource(R.string.exodus_powered),
|
||||||
onClick = onNavigateToDetailsExodus
|
onClick = onNavigateToDetailsExodus
|
||||||
@@ -64,7 +64,7 @@ fun Privacy(report: Report?, onNavigateToDetailsExodus: (() -> Unit)? = null) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PrivacyPreview() {
|
private fun PrivacyPreview() {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Privacy(report = Report(), onNavigateToDetailsExodus = {})
|
Privacy(report = Report(), onNavigateToDetailsExodus = {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import com.aurora.gplayapi.data.models.App
|
|||||||
import com.aurora.gplayapi.data.models.Rating
|
import com.aurora.gplayapi.data.models.Rating
|
||||||
import com.aurora.gplayapi.data.models.Review
|
import com.aurora.gplayapi.data.models.Review
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Header
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
import com.aurora.store.compose.composable.details.RatingListItem
|
import com.aurora.store.compose.composable.details.RatingListItem
|
||||||
import com.aurora.store.compose.composable.details.ReviewListItem
|
import com.aurora.store.compose.composable.details.ReviewListItem
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
@@ -82,7 +82,7 @@ fun RatingAndReviews(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Header(
|
SectionHeader(
|
||||||
title = stringResource(R.string.details_ratings),
|
title = stringResource(R.string.details_ratings),
|
||||||
onClick = onNavigateToDetailsReview
|
onClick = onNavigateToDetailsReview
|
||||||
)
|
)
|
||||||
@@ -90,12 +90,12 @@ fun RatingAndReviews(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_large))
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_large))
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)),
|
modifier = Modifier.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
@@ -114,7 +114,7 @@ fun RatingAndReviews(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
) {
|
) {
|
||||||
stars.reversed().fastForEach { star ->
|
stars.reversed().fastForEach { star ->
|
||||||
RatingListItem(
|
RatingListItem(
|
||||||
@@ -129,8 +129,8 @@ fun RatingAndReviews(
|
|||||||
val pagerState = rememberPagerState { featuredReviews.size }
|
val pagerState = rememberPagerState { featuredReviews.size }
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
|
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
pageSpacing = dimensionResource(R.dimen.margin_medium)
|
pageSpacing = dimensionResource(R.dimen.spacing_medium)
|
||||||
) { page ->
|
) { page ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -158,7 +158,7 @@ private fun RatingAndReviewsPreview(@PreviewParameter(AppPreviewProvider::class)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
RatingAndReviews(rating = app.rating, featuredReviews = reviews)
|
RatingAndReviews(rating = app.rating, featuredReviews = reviews)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
@Composable
|
@Composable
|
||||||
fun Screenshots(screenshots: List<Artwork>, onNavigateToScreenshot: (index: Int) -> Unit = {}) {
|
fun Screenshots(screenshots: List<Artwork>, onNavigateToScreenshot: (index: Int) -> Unit = {}) {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
|
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
) {
|
) {
|
||||||
items(items = screenshots, key = { artwork -> artwork.url }) { artwork ->
|
items(items = screenshots, key = { artwork -> artwork.url }) { artwork ->
|
||||||
ScreenshotListItem(
|
ScreenshotListItem(
|
||||||
@@ -57,7 +57,7 @@ fun Screenshots(screenshots: List<Artwork>, onNavigateToScreenshot: (index: Int)
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ScreenshotsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
private fun ScreenshotsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Screenshots(screenshots = app.screenshots)
|
Screenshots(screenshots = app.screenshots)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ fun Tags(app: App) {
|
|||||||
).filterKeys { it != null }
|
).filterKeys { it != null }
|
||||||
|
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
|
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
items(items = tags.keys.toList()) { label ->
|
items(items = tags.keys.toList()) { label ->
|
||||||
TagListItem(label = label!!, painter = painterResource(tags.getValue(label)))
|
TagListItem(label = label!!, painter = painterResource(tags.getValue(label)))
|
||||||
@@ -77,7 +77,7 @@ fun Tags(app: App) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TagsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
private fun TagsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Tags(app = app)
|
Tags(app = app)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Header
|
|
||||||
import com.aurora.store.compose.composable.Info
|
import com.aurora.store.compose.composable.Info
|
||||||
|
import com.aurora.store.compose.composable.SectionHeader
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
|
||||||
@@ -40,13 +40,13 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Testing(isSubscribed: Boolean, onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}) {
|
fun Testing(isSubscribed: Boolean, onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}) {
|
||||||
Header(title = stringResource(R.string.details_beta))
|
SectionHeader(title = stringResource(R.string.details_beta))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||||
) {
|
) {
|
||||||
Info(
|
Info(
|
||||||
modifier = Modifier.weight(1F),
|
modifier = Modifier.weight(1F),
|
||||||
@@ -79,7 +79,7 @@ fun Testing(isSubscribed: Boolean, onTestingSubscriptionChange: (subscribe: Bool
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TestingPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
private fun TestingPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Testing(isSubscribed = app.testingProgram!!.isSubscribed)
|
Testing(isSubscribed = app.testingProgram!!.isSubscribed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.ui.dev
|
package com.aurora.store.compose.ui.dev
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -31,9 +32,10 @@ import com.aurora.extensions.adaptiveNavigationIcon
|
|||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||||
import com.aurora.store.compose.composable.Error
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.composable.app.LargeAppListItem
|
import com.aurora.store.compose.composable.app.LargeAppListItem
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.viewmodel.details.DevProfileViewModel
|
import com.aurora.store.viewmodel.details.DevProfileViewModel
|
||||||
@@ -48,8 +50,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
@Composable
|
@Composable
|
||||||
fun DevProfileScreen(
|
fun DevProfileScreen(
|
||||||
developerId: String,
|
developerId: String,
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateTo: (Destination) -> Unit,
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
|
||||||
viewModel: DevProfileViewModel = hiltViewModel()
|
viewModel: DevProfileViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
// TODO: Implement when migrating logic for current DevProfileFragment
|
// TODO: Implement when migrating logic for current DevProfileFragment
|
||||||
@@ -62,8 +63,7 @@ fun DevProfileScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
fun DevProfileScreen(
|
fun DevProfileScreen(
|
||||||
publisherId: String,
|
publisherId: String,
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateTo: (Destination) -> Unit,
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
|
||||||
viewModel: SearchViewModel = hiltViewModel()
|
viewModel: SearchViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val apps = viewModel.apps.collectAsLazyPagingItems()
|
val apps = viewModel.apps.collectAsLazyPagingItems()
|
||||||
@@ -73,8 +73,7 @@ fun DevProfileScreen(
|
|||||||
ScreenContent(
|
ScreenContent(
|
||||||
apps = apps,
|
apps = apps,
|
||||||
topAppBarTitle = publisherId,
|
topAppBarTitle = publisherId,
|
||||||
onNavigateUp = onNavigateUp,
|
onNavigateTo = onNavigateTo
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,16 +81,14 @@ fun DevProfileScreen(
|
|||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
apps: LazyPagingItems<App>,
|
apps: LazyPagingItems<App>,
|
||||||
topAppBarTitle: String,
|
topAppBarTitle: String,
|
||||||
onNavigateUp: () -> Unit = {},
|
onNavigateTo: (Destination) -> Unit = {},
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
|
||||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = topAppBarTitle,
|
title = topAppBarTitle,
|
||||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -99,7 +96,7 @@ private fun ScreenContent(
|
|||||||
is LoadState.Loading -> ContainedLoadingIndicator()
|
is LoadState.Loading -> ContainedLoadingIndicator()
|
||||||
|
|
||||||
is LoadState.Error -> {
|
is LoadState.Error -> {
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_disclaimer),
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
message = stringResource(R.string.error)
|
message = stringResource(R.string.error)
|
||||||
@@ -111,13 +108,16 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
.padding(vertical = dimensionResource(R.dimen.spacing_medium)),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
items(count = apps.itemCount, key = apps.itemKey { it.id }) { index ->
|
items(count = apps.itemCount, key = apps.itemKey { it.id }) { index ->
|
||||||
apps[index]?.let { app ->
|
apps[index]?.let { app ->
|
||||||
LargeAppListItem(
|
LargeAppListItem(
|
||||||
app = app,
|
app = app,
|
||||||
onClick = { onNavigateToAppDetails(app.packageName) }
|
onClick = { onNavigateTo(Destination.AppDetails(app.packageName)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import com.aurora.extensions.copyToClipBoard
|
import com.aurora.extensions.copyToClipBoard
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.DispenserListItem
|
import com.aurora.store.compose.composable.DispenserListItem
|
||||||
import com.aurora.store.compose.composable.Error
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.viewmodel.dispenser.DispenserViewModel
|
import com.aurora.store.viewmodel.dispenser.DispenserViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hiltViewModel()) {
|
fun DispenserScreen(viewModel: DispenserViewModel = hiltViewModel()) {
|
||||||
val dispensers by viewModel.dispensers.collectAsStateWithLifecycle()
|
val dispensers by viewModel.dispensers.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
var shouldShowInputDialog by rememberSaveable { mutableStateOf(false) }
|
var shouldShowInputDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -63,7 +63,6 @@ fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hi
|
|||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
dispensers = dispensers,
|
dispensers = dispensers,
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
onAddDispenser = { shouldShowInputDialog = true },
|
onAddDispenser = { shouldShowInputDialog = true },
|
||||||
onRemoveDispenser = { url -> shouldRemoveDispenser = url }
|
onRemoveDispenser = { url -> shouldRemoveDispenser = url }
|
||||||
)
|
)
|
||||||
@@ -71,7 +70,6 @@ fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hi
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
dispensers: Set<String> = emptySet(),
|
dispensers: Set<String> = emptySet(),
|
||||||
onAddDispenser: () -> Unit = {},
|
onAddDispenser: () -> Unit = {},
|
||||||
onRemoveDispenser: (url: String) -> Unit = {}
|
onRemoveDispenser: (url: String) -> Unit = {}
|
||||||
@@ -81,8 +79,7 @@ private fun ScreenContent(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = stringResource(R.string.pref_dispenser_title),
|
title = stringResource(R.string.pref_dispenser_title)
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -97,11 +94,11 @@ private fun ScreenContent(
|
|||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
if (dispensers.isEmpty()) {
|
if (dispensers.isEmpty()) {
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_server),
|
painter = painterResource(R.drawable.ic_server),
|
||||||
message = stringResource(R.string.no_dispensers_available),
|
message = stringResource(R.string.no_dispensers_available),
|
||||||
actionMessage = stringResource(R.string.add_dispenser_title),
|
actionLabel = stringResource(R.string.add_dispenser_title),
|
||||||
onAction = onAddDispenser
|
onAction = onAddDispenser
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +106,7 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
.padding(vertical = dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
items(items = dispensers.toList(), key = { url -> url }) { url ->
|
items(items = dispensers.toList(), key = { url -> url }) { url ->
|
||||||
DispenserListItem(
|
DispenserListItem(
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ fun InputDispenserDialog(onAdd: (url: String) -> Unit = {}, onDismiss: () -> Uni
|
|||||||
title = { Text(text = stringResource(R.string.add_dispenser_title)) },
|
title = { Text(text = stringResource(R.string.add_dispenser_title)) },
|
||||||
text = {
|
text = {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.add_dispenser_summary))
|
Text(text = stringResource(R.string.add_dispenser_summary))
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
@@ -42,13 +41,15 @@ import com.aurora.gplayapi.data.models.App
|
|||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||||
import com.aurora.store.compose.composable.DownloadListItem
|
import com.aurora.store.compose.composable.DownloadListItem
|
||||||
import com.aurora.store.compose.composable.Error
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.ScrollHint
|
import com.aurora.store.compose.composable.ScrollHint
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.compose.ui.downloads.menu.DownloadsMenu
|
import com.aurora.store.compose.ui.downloads.menu.DownloadsMenu
|
||||||
import com.aurora.store.compose.ui.downloads.menu.MenuItem
|
import com.aurora.store.compose.ui.downloads.menu.MenuItem
|
||||||
|
import com.aurora.store.compose.ui.sheets.DownloadActionsSheet
|
||||||
import com.aurora.store.data.model.DownloadStatus
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
import com.aurora.store.data.room.download.Download
|
import com.aurora.store.data.room.download.Download
|
||||||
import com.aurora.store.viewmodel.downloads.DownloadsViewModel
|
import com.aurora.store.viewmodel.downloads.DownloadsViewModel
|
||||||
@@ -57,9 +58,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadsScreen(
|
fun DownloadsScreen(
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateTo: (Destination) -> Unit,
|
||||||
viewModel: DownloadsViewModel = hiltViewModel(),
|
viewModel: DownloadsViewModel = hiltViewModel()
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val downloads = viewModel.downloads.collectAsLazyPagingItems()
|
val downloads = viewModel.downloads.collectAsLazyPagingItems()
|
||||||
@@ -79,9 +79,8 @@ fun DownloadsScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
downloads = downloads,
|
downloads = downloads,
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails,
|
onNavigateTo = onNavigateTo,
|
||||||
onCancelAll = { viewModel.cancelAll() },
|
onCancelAll = { viewModel.cancelAll() },
|
||||||
onForceClearAll = { viewModel.clearAll() },
|
onForceClearAll = { viewModel.clearAll() },
|
||||||
onClearFinished = { viewModel.clearFinished() },
|
onClearFinished = { viewModel.clearFinished() },
|
||||||
@@ -100,8 +99,7 @@ fun DownloadsScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
downloads: LazyPagingItems<Download> = emptyPagingItems(),
|
downloads: LazyPagingItems<Download> = emptyPagingItems(),
|
||||||
onNavigateUp: () -> Unit = {},
|
onNavigateTo: (Destination) -> Unit = {},
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
|
||||||
onCancel: (packageName: String) -> Unit = {},
|
onCancel: (packageName: String) -> Unit = {},
|
||||||
onClear: (download: Download) -> Unit = {},
|
onClear: (download: Download) -> Unit = {},
|
||||||
onExport: (download: Download) -> Unit = {},
|
onExport: (download: Download) -> Unit = {},
|
||||||
@@ -117,6 +115,19 @@ private fun ScreenContent(
|
|||||||
* Save the initial loading state to make sure we don't replay the loading animation again.
|
* Save the initial loading state to make sure we don't replay the loading animation again.
|
||||||
*/
|
*/
|
||||||
var initialLoad by rememberSaveable { mutableStateOf(true) }
|
var initialLoad by rememberSaveable { mutableStateOf(true) }
|
||||||
|
var actionsTarget by rememberSaveable { mutableStateOf<Download?>(null) }
|
||||||
|
|
||||||
|
actionsTarget?.let { target ->
|
||||||
|
DownloadActionsSheet(
|
||||||
|
download = target,
|
||||||
|
onDismiss = { actionsTarget = null },
|
||||||
|
onShowDetails = { onNavigateTo(Destination.AppDetails(target.packageName)) },
|
||||||
|
onCancel = { onCancel(target.packageName) },
|
||||||
|
onInstall = { onInstall(target) },
|
||||||
|
onExport = { onExport(target) },
|
||||||
|
onClear = { onClear(target) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SetupMenu() {
|
fun SetupMenu() {
|
||||||
@@ -133,7 +144,6 @@ private fun ScreenContent(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = stringResource(R.string.title_download_manager),
|
title = stringResource(R.string.title_download_manager),
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
actions = { if (downloads.itemCount != 0) SetupMenu() }
|
actions = { if (downloads.itemCount != 0) SetupMenu() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -142,7 +152,7 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
.padding(vertical = dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
downloads.loadState.refresh is LoadState.Loading && initialLoad -> {
|
downloads.loadState.refresh is LoadState.Loading && initialLoad -> {
|
||||||
@@ -153,7 +163,7 @@ private fun ScreenContent(
|
|||||||
initialLoad = false
|
initialLoad = false
|
||||||
|
|
||||||
if (downloads.itemCount == 0) {
|
if (downloads.itemCount == 0) {
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_download_manager),
|
painter = painterResource(R.drawable.ic_download_manager),
|
||||||
message = stringResource(R.string.download_none)
|
message = stringResource(R.string.download_none)
|
||||||
@@ -172,20 +182,13 @@ private fun ScreenContent(
|
|||||||
DownloadListItem(
|
DownloadListItem(
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
download = download,
|
download = download,
|
||||||
onClick = {
|
onClick = { actionsTarget = download }
|
||||||
onNavigateToAppDetails(download.packageName)
|
|
||||||
},
|
|
||||||
onClear = { onClear(download) },
|
|
||||||
onCancel = { onCancel(download.packageName) },
|
|
||||||
onExport = { onExport(download) },
|
|
||||||
onInstall = { onInstall(download) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
@@ -41,10 +40,11 @@ import com.aurora.extensions.emptyPagingItems
|
|||||||
import com.aurora.extensions.toast
|
import com.aurora.extensions.toast
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||||
import com.aurora.store.compose.composable.Error
|
|
||||||
import com.aurora.store.compose.composable.FavouriteListItem
|
import com.aurora.store.compose.composable.FavouriteListItem
|
||||||
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.ScrollHint
|
import com.aurora.store.compose.composable.ScrollHint
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.compose.preview.FavouritePreviewProvider
|
import com.aurora.store.compose.preview.FavouritePreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.compose.ui.favourite.menu.FavouriteMenu
|
import com.aurora.store.compose.ui.favourite.menu.FavouriteMenu
|
||||||
@@ -57,8 +57,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FavouriteScreen(
|
fun FavouriteScreen(
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateTo: (Destination) -> Unit,
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
|
||||||
viewModel: FavouriteViewModel = hiltViewModel()
|
viewModel: FavouriteViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -90,8 +89,7 @@ fun FavouriteScreen(
|
|||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
favourites = favourites,
|
favourites = favourites,
|
||||||
onNavigateUp = onNavigateUp,
|
onNavigateTo = onNavigateTo,
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails,
|
|
||||||
onRemoveFavourite = { packageName -> viewModel.removeFavourite(packageName) },
|
onRemoveFavourite = { packageName -> viewModel.removeFavourite(packageName) },
|
||||||
onImportFavourites = {
|
onImportFavourites = {
|
||||||
documentOpenLauncher.launch(arrayOf(JSON_MIME_TYPE))
|
documentOpenLauncher.launch(arrayOf(JSON_MIME_TYPE))
|
||||||
@@ -107,8 +105,7 @@ fun FavouriteScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
favourites: LazyPagingItems<Favourite> = emptyPagingItems(),
|
favourites: LazyPagingItems<Favourite> = emptyPagingItems(),
|
||||||
onNavigateUp: () -> Unit = {},
|
onNavigateTo: (Destination) -> Unit = {},
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
|
||||||
onRemoveFavourite: (packageName: String) -> Unit = {},
|
onRemoveFavourite: (packageName: String) -> Unit = {},
|
||||||
onImportFavourites: () -> Unit = {},
|
onImportFavourites: () -> Unit = {},
|
||||||
onExportFavourites: () -> Unit = {}
|
onExportFavourites: () -> Unit = {}
|
||||||
@@ -135,7 +132,6 @@ private fun ScreenContent(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = stringResource(R.string.title_favourites_manager),
|
title = stringResource(R.string.title_favourites_manager),
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
actions = { SetupMenu() }
|
actions = { SetupMenu() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -144,7 +140,7 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
.padding(vertical = dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
favourites.loadState.refresh is LoadState.Loading && initialLoad -> {
|
favourites.loadState.refresh is LoadState.Loading && initialLoad -> {
|
||||||
@@ -155,7 +151,7 @@ private fun ScreenContent(
|
|||||||
initialLoad = false
|
initialLoad = false
|
||||||
|
|
||||||
if (favourites.itemCount == 0) {
|
if (favourites.itemCount == 0) {
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_favorite_unchecked),
|
painter = painterResource(R.drawable.ic_favorite_unchecked),
|
||||||
message = stringResource(R.string.details_no_favourites)
|
message = stringResource(R.string.details_no_favourites)
|
||||||
@@ -175,7 +171,9 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
favourite = favourite,
|
favourite = favourite,
|
||||||
onClick = {
|
onClick = {
|
||||||
onNavigateToAppDetails(favourite.packageName)
|
onNavigateTo(
|
||||||
|
Destination.AppDetails(favourite.packageName)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onClear = { onRemoveFavourite(favourite.packageName) }
|
onClear = { onRemoveFavourite(favourite.packageName) }
|
||||||
)
|
)
|
||||||
@@ -184,7 +182,6 @@ private fun ScreenContent(
|
|||||||
}
|
}
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,27 +6,32 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.ui.installed
|
package com.aurora.store.compose.ui.installed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
@@ -36,36 +41,48 @@ import com.aurora.extensions.emptyPagingItems
|
|||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||||
import com.aurora.store.compose.composable.Error
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.ScrollHint
|
import com.aurora.store.compose.composable.ScrollHint
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.composable.app.LargeAppListItem
|
import com.aurora.store.compose.composable.app.InstalledAppListItem
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import com.aurora.store.compose.ui.commons.InstalledAppMeta
|
||||||
|
import com.aurora.store.compose.ui.commons.SortFilterSheet
|
||||||
|
import com.aurora.store.compose.ui.commons.SortFilterState
|
||||||
import com.aurora.store.viewmodel.all.InstalledViewModel
|
import com.aurora.store.viewmodel.all.InstalledViewModel
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InstalledScreen(
|
fun InstalledScreen(
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateTo: (Destination) -> Unit,
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
|
||||||
viewModel: InstalledViewModel = hiltViewModel()
|
viewModel: InstalledViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val apps = viewModel.apps.collectAsLazyPagingItems()
|
val apps = viewModel.apps.collectAsLazyPagingItems()
|
||||||
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val installers by viewModel.installers.collectAsStateWithLifecycle()
|
||||||
|
val metadata by viewModel.metadata.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
apps = apps,
|
apps = apps,
|
||||||
onNavigateUp = onNavigateUp,
|
state = state,
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails
|
installers = installers,
|
||||||
|
metadata = metadata,
|
||||||
|
onStateChange = viewModel::updateState,
|
||||||
|
onNavigateTo = onNavigateTo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
apps: LazyPagingItems<App> = emptyPagingItems(),
|
apps: LazyPagingItems<App> = emptyPagingItems(),
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit = {}
|
state: SortFilterState = SortFilterState(),
|
||||||
|
installers: Map<String, String> = emptyMap(),
|
||||||
|
metadata: Map<String, InstalledAppMeta> = emptyMap(),
|
||||||
|
onStateChange: (SortFilterState) -> Unit = {},
|
||||||
|
onNavigateTo: (Destination) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
* For some reason paging3 frequently out-of-nowhere invalidates the list which causes
|
* For some reason paging3 frequently out-of-nowhere invalidates the list which causes
|
||||||
@@ -74,12 +91,20 @@ private fun ScreenContent(
|
|||||||
* Save the initial loading state to make sure we don't replay the loading animation again.
|
* Save the initial loading state to make sure we don't replay the loading animation again.
|
||||||
*/
|
*/
|
||||||
var initialLoad by rememberSaveable { mutableStateOf(true) }
|
var initialLoad by rememberSaveable { mutableStateOf(true) }
|
||||||
|
var sheetVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = stringResource(R.string.title_apps_games),
|
title = stringResource(R.string.title_apps_games),
|
||||||
onNavigateUp = onNavigateUp
|
actions = {
|
||||||
|
IconButton(onClick = { sheetVisible = true }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_tune),
|
||||||
|
contentDescription = stringResource(R.string.installed_sort_filter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -97,7 +122,7 @@ private fun ScreenContent(
|
|||||||
initialLoad = false
|
initialLoad = false
|
||||||
|
|
||||||
if (apps.itemCount == 0) {
|
if (apps.itemCount == 0) {
|
||||||
Error(
|
Placeholder(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
painter = painterResource(R.drawable.ic_apps_outage),
|
painter = painterResource(R.drawable.ic_apps_outage),
|
||||||
message = stringResource(R.string.no_apps_available)
|
message = stringResource(R.string.no_apps_available)
|
||||||
@@ -106,23 +131,34 @@ private fun ScreenContent(
|
|||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState
|
state = listState,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
count = apps.itemCount,
|
count = apps.itemCount,
|
||||||
key = apps.itemKey { it.packageName }
|
key = apps.itemKey { it.packageName }
|
||||||
) { index ->
|
) { index ->
|
||||||
apps[index]?.let { app ->
|
apps[index]?.let { app ->
|
||||||
LargeAppListItem(
|
val meta = metadata[app.packageName]
|
||||||
|
InstalledAppListItem(
|
||||||
app = app,
|
app = app,
|
||||||
onClick = { onNavigateToAppDetails(app.packageName) }
|
sizeBytes = meta?.sizeBytes ?: 0L,
|
||||||
|
lastUpdateTime = meta?.lastUpdateTime ?: 0L,
|
||||||
|
installerLabel = meta?.installer
|
||||||
|
?.let { installers[it] },
|
||||||
|
onClick = {
|
||||||
|
onNavigateTo(
|
||||||
|
Destination.AppDetails(app.packageName)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -131,6 +167,15 @@ private fun ScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sheetVisible) {
|
||||||
|
SortFilterSheet(
|
||||||
|
state = state,
|
||||||
|
installers = installers,
|
||||||
|
onStateChange = onStateChange,
|
||||||
|
onDismiss = { sheetVisible = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
|||||||
241
app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt
Normal file
241
app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.main
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material3.Badge
|
||||||
|
import androidx.compose.material3.BadgedBox
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.aurora.extensions.requiresObbDir
|
||||||
|
import com.aurora.store.MainViewModel
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.composition.LocalNetworkStatus
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
|
import com.aurora.store.compose.ui.apps.AppsGamesScreen
|
||||||
|
import com.aurora.store.compose.ui.commons.MoreSheet
|
||||||
|
import com.aurora.store.compose.ui.commons.NetworkScreen
|
||||||
|
import com.aurora.store.compose.ui.sheets.AppUpdateSheet
|
||||||
|
import com.aurora.store.compose.ui.updates.UpdatesScreen
|
||||||
|
import com.aurora.store.data.model.NetworkStatus
|
||||||
|
import com.aurora.store.data.model.PermissionType
|
||||||
|
import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted
|
||||||
|
import com.aurora.store.data.room.update.Update
|
||||||
|
import com.aurora.store.viewmodel.all.UpdatesViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private enum class MainTab(
|
||||||
|
@StringRes val labelRes: Int,
|
||||||
|
@DrawableRes val iconRes: Int
|
||||||
|
) {
|
||||||
|
APPS(R.string.title_apps, R.drawable.ic_apps),
|
||||||
|
GAMES(R.string.title_games, R.drawable.ic_games),
|
||||||
|
UPDATES(R.string.title_updates, R.drawable.ic_updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(
|
||||||
|
initialTab: Int = 0,
|
||||||
|
mainViewModel: MainViewModel = hiltViewModel(),
|
||||||
|
updatesViewModel: UpdatesViewModel = hiltViewModel(),
|
||||||
|
onNavigateTo: (Destination) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val networkStatus = LocalNetworkStatus.current
|
||||||
|
val updates by mainViewModel.updateHelper.updates.collectAsStateWithLifecycle(
|
||||||
|
initialValue = null
|
||||||
|
)
|
||||||
|
val updateCount = updates?.size ?: 0
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = initialTab.coerceIn(
|
||||||
|
0,
|
||||||
|
MainTab.entries.size - 1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
MainTab.entries.size
|
||||||
|
}
|
||||||
|
|
||||||
|
var showMoreSheet by remember { mutableStateOf(false) }
|
||||||
|
var appUpdateTarget by remember { mutableStateOf<Update?>(null) }
|
||||||
|
|
||||||
|
fun handleNavigation(destination: Destination) {
|
||||||
|
when (destination) {
|
||||||
|
is Destination.AppUpdate -> appUpdateTarget = destination.update
|
||||||
|
else -> onNavigateTo(destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkStatus == NetworkStatus.UNAVAILABLE) {
|
||||||
|
NetworkScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showMoreSheet) {
|
||||||
|
MoreSheet(
|
||||||
|
onDismiss = { showMoreSheet = false },
|
||||||
|
onNavigateTo = { destination ->
|
||||||
|
showMoreSheet = false
|
||||||
|
onNavigateTo(destination)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
appUpdateTarget?.let { app ->
|
||||||
|
AppUpdateSheet(
|
||||||
|
update = app,
|
||||||
|
onDismiss = { appUpdateTarget = null },
|
||||||
|
onNavigateTo = { destination ->
|
||||||
|
appUpdateTarget = null
|
||||||
|
onNavigateTo(destination)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = stringResource(MainTab.entries[pagerState.currentPage].labelRes),
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { onNavigateTo(Destination.Downloads) }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_download_manager),
|
||||||
|
contentDescription = stringResource(R.string.title_download_manager)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { showMoreSheet = true }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_settings_account),
|
||||||
|
contentDescription = stringResource(R.string.title_more)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingActionButton(onClick = { onNavigateTo(Destination.Search) }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_round_search),
|
||||||
|
contentDescription = stringResource(R.string.action_search)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
NavigationBar {
|
||||||
|
MainTab.entries.forEachIndexed { index, tab ->
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = pagerState.currentPage == index,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
if (tab == MainTab.UPDATES && updateCount > 0) {
|
||||||
|
BadgedBox(badge = { Badge { Text("$updateCount") } }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(tab.iconRes),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(tab.iconRes),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(tab.labelRes)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.consumeWindowInsets(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
userScrollEnabled = false,
|
||||||
|
beyondViewportPageCount = MainTab.entries.size - 1,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) { page ->
|
||||||
|
when (MainTab.entries[page]) {
|
||||||
|
MainTab.APPS -> AppsGamesScreen(
|
||||||
|
pageType = 0,
|
||||||
|
onNavigateTo = onNavigateTo
|
||||||
|
)
|
||||||
|
MainTab.GAMES -> AppsGamesScreen(
|
||||||
|
pageType = 1,
|
||||||
|
onNavigateTo = ::handleNavigation
|
||||||
|
)
|
||||||
|
MainTab.UPDATES -> UpdatesScreen(
|
||||||
|
viewModel = updatesViewModel,
|
||||||
|
onNavigateTo = ::handleNavigation,
|
||||||
|
onRequestUpdate = { update ->
|
||||||
|
if (update.fileList.requiresObbDir() &&
|
||||||
|
!isGranted(context, PermissionType.STORAGE_MANAGER)
|
||||||
|
) {
|
||||||
|
onNavigateTo(
|
||||||
|
Destination.PermissionRationale(
|
||||||
|
setOf(PermissionType.STORAGE_MANAGER)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
updatesViewModel.download(update)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequestUpdateAll = { selectedUpdates ->
|
||||||
|
val needsObb = selectedUpdates.any {
|
||||||
|
it.fileList.requiresObbDir()
|
||||||
|
}
|
||||||
|
if (needsObb && !isGranted(context, PermissionType.STORAGE_MANAGER)) {
|
||||||
|
onNavigateTo(
|
||||||
|
Destination.PermissionRationale(
|
||||||
|
setOf(PermissionType.STORAGE_MANAGER)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
updatesViewModel.downloadAll(selectedUpdates)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancelUpdate = { packageName ->
|
||||||
|
updatesViewModel.cancelDownload(packageName)
|
||||||
|
},
|
||||||
|
onCancelAll = { updatesViewModel.cancelAll() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import com.aurora.extensions.toast
|
import com.aurora.extensions.toast
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
@@ -62,12 +61,12 @@ private fun ScreenContent(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
state = listState
|
state = listState
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
|
modifier = Modifier.padding(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.onboarding_title_gsf),
|
text = stringResource(R.string.onboarding_title_gsf),
|
||||||
@@ -105,7 +104,6 @@ private fun ScreenContent(
|
|||||||
|
|
||||||
ScrollHint(
|
ScrollHint(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
bottomPadding = 5.dp,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,16 +99,16 @@ private fun ScreenContent(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.padding(
|
.padding(
|
||||||
vertical = dimensionResource(R.dimen.padding_medium),
|
vertical = dimensionResource(R.dimen.spacing_medium),
|
||||||
horizontal = if (windowAdaptiveInfo.isWindowCompact) {
|
horizontal = if (windowAdaptiveInfo.isWindowCompact) {
|
||||||
dimensionResource(R.dimen.padding_medium)
|
dimensionResource(R.dimen.spacing_medium)
|
||||||
} else {
|
} else {
|
||||||
dimensionResource(R.dimen.padding_xlarge)
|
dimensionResource(R.dimen.spacing_xlarge)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.padding_medium),
|
dimensionResource(R.dimen.spacing_medium),
|
||||||
Alignment.End
|
Alignment.End
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ private fun PageContent(
|
|||||||
onPermissionCallback: (type: PermissionType) -> Unit = {}
|
onPermissionCallback: (type: PermissionType) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
PermissionList(
|
PermissionList(
|
||||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
permissions = permissions,
|
permissions = permissions,
|
||||||
onPermissionCallback = onPermissionCallback,
|
onPermissionCallback = onPermissionCallback,
|
||||||
header = {
|
header = {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
|
modifier = Modifier.padding(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.onboarding_title_permissions),
|
text = stringResource(R.string.onboarding_title_permissions),
|
||||||
|
|||||||
@@ -96,12 +96,12 @@ private fun PageContent(onAboutAurora: () -> Unit = {}) {
|
|||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
|
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||||
) {
|
) {
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
|
modifier = Modifier.padding(dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.onboarding_title_welcome),
|
text = stringResource(R.string.onboarding_title_welcome),
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.preferences
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import com.aurora.store.data.model.PermissionType
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(onNavigateTo: (Destination) -> Unit) {
|
||||||
|
ScreenContent(onNavigateTo = onNavigateTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScreenContent(onNavigateTo: (Destination) -> Unit = {}) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = stringResource(R.string.title_settings)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigateTo(
|
||||||
|
Destination.PermissionRationale(PermissionType.entries.toSet())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_list_check),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(stringResource(R.string.onboarding_title_permissions))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigateTo(Destination.InstallationPreference)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_installation),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.title_installation)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { onNavigateTo(Destination.UIPreference) },
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_ui),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.pref_ui_title)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { onNavigateTo(Destination.NetworkPreference) },
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_network),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.pref_network_title)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { onNavigateTo(Destination.UpdatesPreference) },
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_updates),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.title_updates)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun SettingsScreenPreview() {
|
||||||
|
ScreenContent()
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.preferences
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLocale
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.aurora.extensions.isTAndAbove
|
||||||
|
import com.aurora.extensions.setAppTheme
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog
|
||||||
|
import com.aurora.store.util.Preferences
|
||||||
|
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
|
||||||
|
import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU
|
||||||
|
import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE
|
||||||
|
import com.aurora.store.util.save
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UIPreferenceScreen() {
|
||||||
|
ScreenContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScreenContent() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val themeEntries = stringArrayResource(R.array.pref_theme_style)
|
||||||
|
var themeStyle by remember {
|
||||||
|
mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_THEME_STYLE))
|
||||||
|
}
|
||||||
|
val tabEntries = stringArrayResource(R.array.pref_default_tab)
|
||||||
|
var selectedTab by remember {
|
||||||
|
mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_DEFAULT_SELECTED_TAB))
|
||||||
|
}
|
||||||
|
var forYou by remember {
|
||||||
|
mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FOR_YOU, true))
|
||||||
|
}
|
||||||
|
var showThemeDialog by remember { mutableStateOf(false) }
|
||||||
|
var showTabDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showThemeDialog) {
|
||||||
|
SingleChoiceDialog(
|
||||||
|
title = stringResource(R.string.pref_ui_theme),
|
||||||
|
options = themeEntries.toList(),
|
||||||
|
selected = themeStyle,
|
||||||
|
onSelect = { index ->
|
||||||
|
themeStyle = index
|
||||||
|
context.save(PREFERENCE_THEME_STYLE, index)
|
||||||
|
setAppTheme(index)
|
||||||
|
showThemeDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showThemeDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTabDialog) {
|
||||||
|
SingleChoiceDialog(
|
||||||
|
title = stringResource(R.string.pref_ui_layout_tab),
|
||||||
|
options = tabEntries.toList(),
|
||||||
|
selected = selectedTab,
|
||||||
|
onSelect = { index ->
|
||||||
|
selectedTab = index
|
||||||
|
context.save(PREFERENCE_DEFAULT_SELECTED_TAB, index)
|
||||||
|
showTabDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showTabDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = stringResource(R.string.pref_ui_title)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
if (isTAndAbove) {
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply {
|
||||||
|
data = ("package:" + context.packageName).toUri()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.app_language)) },
|
||||||
|
supportingContent = { Text(LocalLocale.current.platformLocale.displayName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item { HorizontalDivider() }
|
||||||
|
item {
|
||||||
|
ListItem(headlineContent = { Text(stringResource(R.string.pref_ui_theme)) })
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { showThemeDialog = true },
|
||||||
|
headlineContent = { Text(stringResource(R.string.pref_ui_theme)) },
|
||||||
|
supportingContent = { Text(themeEntries.getOrElse(themeStyle) { "" }) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { HorizontalDivider() }
|
||||||
|
item {
|
||||||
|
ListItem(headlineContent = { Text(stringResource(R.string.pref_ui_layout)) })
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { showTabDialog = true },
|
||||||
|
headlineContent = { Text(stringResource(R.string.pref_ui_layout_tab)) },
|
||||||
|
supportingContent = { Text(tabEntries.getOrElse(selectedTab) { "" }) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { HorizontalDivider() }
|
||||||
|
item {
|
||||||
|
ListItem(headlineContent = { Text(stringResource(R.string.pref_common_extra)) })
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
forYou = !forYou
|
||||||
|
context.save(PREFERENCE_FOR_YOU, forYou)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.pref_ui_no_for_you)) },
|
||||||
|
supportingContent = { Text(stringResource(R.string.pref_ui_no_for_you_desc)) },
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = forYou,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
forYou = checked
|
||||||
|
context.save(PREFERENCE_FOR_YOU, checked)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun UIPreferenceScreenPreview() {
|
||||||
|
ScreenContent()
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.preferences.installation
|
||||||
|
|
||||||
|
import android.app.admin.DevicePolicyManager
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
|
import com.aurora.store.util.Preferences
|
||||||
|
import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE
|
||||||
|
import com.aurora.store.util.save
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InstallationPreferenceScreen(onNavigateTo: (Destination) -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val devicePolicyManager = context.getSystemService<DevicePolicyManager>()
|
||||||
|
val isDeviceOwner = devicePolicyManager?.isDeviceOwnerApp(context.packageName) ?: false
|
||||||
|
|
||||||
|
ScreenContent(
|
||||||
|
onNavigateTo = onNavigateTo,
|
||||||
|
isDeviceOwner = isDeviceOwner,
|
||||||
|
onClearDeviceOwner = {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
devicePolicyManager?.clearDeviceOwnerApp(context.packageName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScreenContent(
|
||||||
|
onNavigateTo: (Destination) -> Unit = {},
|
||||||
|
isDeviceOwner: Boolean = false,
|
||||||
|
onClearDeviceOwner: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var autoDelete by remember {
|
||||||
|
mutableStateOf(Preferences.getBoolean(context, PREFERENCE_AUTO_DELETE, true))
|
||||||
|
}
|
||||||
|
var showClearOwnerDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showClearOwnerDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showClearOwnerDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.pref_clear_device_owner_title)) },
|
||||||
|
text = { Text(stringResource(R.string.pref_clear_device_owner_desc)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showClearOwnerDialog = false
|
||||||
|
onClearDeviceOwner()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(android.R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showClearOwnerDialog = false }) {
|
||||||
|
Text(stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = stringResource(R.string.title_installation)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { onNavigateTo(Destination.Installer) },
|
||||||
|
headlineContent = { Text(stringResource(R.string.pref_install_mode_title)) },
|
||||||
|
supportingContent = { Text(stringResource(R.string.pref_install_mode_summary)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { HorizontalDivider() }
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.pref_common_extra)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
autoDelete = !autoDelete
|
||||||
|
context.save(PREFERENCE_AUTO_DELETE, autoDelete)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.pref_install_delete_title)) },
|
||||||
|
supportingContent = {
|
||||||
|
Text(stringResource(R.string.pref_install_delete_summary))
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = autoDelete,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
autoDelete = checked
|
||||||
|
context.save(PREFERENCE_AUTO_DELETE, checked)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isDeviceOwner) {
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { showClearOwnerDialog = true },
|
||||||
|
headlineContent = {
|
||||||
|
Text(stringResource(R.string.pref_clear_device_owner_title))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(stringResource(R.string.pref_clear_device_owner_summary))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun InstallationPreferenceScreenPreview() {
|
||||||
|
ScreenContent()
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ import com.aurora.store.data.model.InstallerInfo
|
|||||||
import com.aurora.store.viewmodel.preferences.InstallerViewModel
|
import com.aurora.store.viewmodel.preferences.InstallerViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hiltViewModel()) {
|
fun InstallerScreen(viewModel: InstallerViewModel = hiltViewModel()) {
|
||||||
val currentInstallerId by viewModel.currentInstaller.collectAsStateWithLifecycle()
|
val currentInstallerId by viewModel.currentInstaller.collectAsStateWithLifecycle()
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
@@ -46,7 +46,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi
|
|||||||
}
|
}
|
||||||
|
|
||||||
ScreenContent(
|
ScreenContent(
|
||||||
onNavigateUp = onNavigateUp,
|
|
||||||
snackBarHostState = snackBarHostState,
|
snackBarHostState = snackBarHostState,
|
||||||
currentInstaller = Installer.entries[currentInstallerId],
|
currentInstaller = Installer.entries[currentInstallerId],
|
||||||
availableInstallers = AppInstaller.getAvailableInstallersInfo(LocalContext.current),
|
availableInstallers = AppInstaller.getAvailableInstallersInfo(LocalContext.current),
|
||||||
@@ -56,7 +55,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
onNavigateUp: () -> Unit = {},
|
|
||||||
snackBarHostState: SnackbarHostState = SnackbarHostState(),
|
snackBarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
currentInstaller: Installer = Installer.SESSION,
|
currentInstaller: Installer = Installer.SESSION,
|
||||||
availableInstallers: List<InstallerInfo> = emptyList(),
|
availableInstallers: List<InstallerInfo> = emptyList(),
|
||||||
@@ -70,8 +68,7 @@ private fun ScreenContent(
|
|||||||
},
|
},
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = stringResource(R.string.pref_install_mode_title),
|
title = stringResource(R.string.pref_install_mode_title)
|
||||||
onNavigateUp = onNavigateUp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -79,7 +76,7 @@ private fun ScreenContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
.padding(vertical = dimensionResource(R.dimen.spacing_medium))
|
||||||
) {
|
) {
|
||||||
items(items = availableInstallers, key = { i -> i.id }) { installerInfo ->
|
items(items = availableInstallers, key = { i -> i.id }) { installerInfo ->
|
||||||
InstallerListItem(
|
InstallerListItem(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user