Merge branch 'dev' into 'master'
dev See merge request AuroraOSS/AuroraStore!568
This commit is contained in:
@@ -14,7 +14,6 @@ plugins {
|
||||
alias(libs.plugins.jetbrains.kotlin.parcelize)
|
||||
alias(libs.plugins.jetbrains.kotlin.serialization)
|
||||
alias(libs.plugins.google.ksp)
|
||||
alias(libs.plugins.androidx.navigation)
|
||||
alias(libs.plugins.ktlint)
|
||||
alias(libs.plugins.rikka.tools.refine.plugin)
|
||||
alias(libs.plugins.hilt.android.plugin)
|
||||
@@ -128,23 +127,25 @@ configure<ApplicationExtension> {
|
||||
create("vanilla") {
|
||||
isDefault = true
|
||||
dimension = "device"
|
||||
buildConfigField("Boolean", "SHOW_ANONYMOUS_LOGIN", "true")
|
||||
}
|
||||
|
||||
create("huawei") {
|
||||
dimension = "device"
|
||||
versionNameSuffix = "-hw"
|
||||
buildConfigField("Boolean", "SHOW_ANONYMOUS_LOGIN", "false")
|
||||
}
|
||||
|
||||
// This flavor is only for preloaded devices / users who push the app to system
|
||||
create("preload") {
|
||||
dimension = "device"
|
||||
versionNameSuffix = "-preload"
|
||||
buildConfigField("Boolean", "SHOW_ANONYMOUS_LOGIN", "true")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
aidl = true
|
||||
compose = true
|
||||
}
|
||||
@@ -193,8 +194,6 @@ dependencies {
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.lifecycle.navigation3)
|
||||
implementation(libs.androidx.preference.ktx)
|
||||
implementation(libs.androidx.swiperefreshlayout)
|
||||
implementation(libs.androidx.viewpager2)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.paging.runtime)
|
||||
|
||||
@@ -205,13 +204,12 @@ dependencies {
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.androidx.navigation.fragment.ktx)
|
||||
implementation(libs.androidx.navigation.ui.ktx)
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.compose.runtime.livedata)
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
@@ -225,13 +223,6 @@ dependencies {
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.network)
|
||||
|
||||
// Shimmer
|
||||
implementation(libs.facebook.shimmer)
|
||||
|
||||
// Epoxy
|
||||
implementation(libs.airbnb.epoxy.android)
|
||||
ksp(libs.airbnb.epoxy.processor)
|
||||
|
||||
// HTTP Clients
|
||||
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">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".ComposeActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -103,16 +103,28 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||
</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 to host composable screens -->
|
||||
<activity
|
||||
android:name=".ComposeActivity"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<!-- Notification Action (Download Complete) -->
|
||||
<activity
|
||||
android:name=".data.activity.InstallActivity"
|
||||
|
||||
@@ -54,6 +54,15 @@ val isVAndAbove: Boolean
|
||||
val isMIUI: Boolean
|
||||
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
|
||||
get() = Build.MANUFACTURER.lowercase(Locale.getDefault()).contains("huawei") ||
|
||||
Build.HARDWARE.lowercase(Locale.getDefault()).contains("kirin") ||
|
||||
|
||||
@@ -6,43 +6,43 @@
|
||||
|
||||
package com.aurora.store
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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.UI
|
||||
import com.aurora.store.compose.navigation.NavDisplay
|
||||
import com.aurora.store.compose.navigation.Screen
|
||||
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.Preferences
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ComposeActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var networkProvider: NetworkProvider
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
MigrationReceiver.runMigrationsIfRequired(this)
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Ensure the classloader is set for the Screen parcelable
|
||||
intent.setExtrasClassLoader(Screen::class.java.classLoader)
|
||||
|
||||
var startDestination = IntentCompat.getParcelableExtra(
|
||||
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 startDestination = resolveStartDestination()
|
||||
|
||||
val localUI = when {
|
||||
PackageUtil.isTv(this) -> UI.TV
|
||||
@@ -50,11 +50,45 @@ class ComposeActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
setContent {
|
||||
CompositionLocalProvider(LocalUI provides localUI) {
|
||||
val networkStatus by networkProvider.status.collectAsStateWithLifecycle(
|
||||
initialValue = NetworkStatus.AVAILABLE
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalUI provides localUI,
|
||||
LocalNetworkStatus provides networkStatus
|
||||
) {
|
||||
AuroraTheme {
|
||||
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 com.aurora.store.data.helper.UpdateHelper
|
||||
import com.aurora.store.data.providers.NetworkProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
val networkProvider: NetworkProvider,
|
||||
val updateHelper: UpdateHelper
|
||||
) : 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.Color
|
||||
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.material3.Checkbox
|
||||
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.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
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 androidx.core.graphics.drawable.toBitmap
|
||||
@@ -33,18 +26,6 @@ import com.aurora.store.BuildConfig
|
||||
import com.aurora.store.R
|
||||
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
|
||||
fun BlackListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -57,50 +38,30 @@ fun BlackListItem(
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = isEnabled, onClick = onClick)
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_xsmall)
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(modifier = Modifier.weight(1F)) {
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = displayName,
|
||||
supporting = packageName,
|
||||
tertiary = stringResource(R.string.version, versionName, versionCode),
|
||||
headlineStyle = MaterialTheme.typography.bodyMedium,
|
||||
onClick = onClick,
|
||||
enabled = isEnabled,
|
||||
leading = {
|
||||
Image(
|
||||
bitmap = icon.asImageBitmap(),
|
||||
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))
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
},
|
||||
trailing = {
|
||||
Checkbox(checked = isChecked, enabled = isEnabled, onCheckedChange = { onClick() })
|
||||
}
|
||||
Checkbox(checked = isChecked, enabled = isEnabled, onCheckedChange = { onClick() })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val DESATURATE = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) })
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@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(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(dimensionResource(R.dimen.padding_small)),
|
||||
.padding(dimensionResource(R.dimen.spacing_small)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val description = stringResource(R.string.loading)
|
||||
|
||||
@@ -5,34 +5,13 @@
|
||||
|
||||
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.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.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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
|
||||
fun DeviceListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -43,34 +22,16 @@ fun DeviceListItem(
|
||||
isChecked: Boolean = false,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { if (!isChecked) onClick() })
|
||||
.padding(dimensionResource(R.dimen.padding_small)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
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
|
||||
)
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = userReadableName,
|
||||
supporting = stringResource(R.string.spoof_property, manufacturer, androidVersionSdk),
|
||||
tertiary = platforms.replace(",\\s*".toRegex(), ", "),
|
||||
onClick = { if (!isChecked) onClick() },
|
||||
trailing = {
|
||||
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
||||
}
|
||||
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
||||
@@ -5,32 +5,17 @@
|
||||
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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
|
||||
fun DispenserListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -38,37 +23,23 @@ fun DispenserListItem(
|
||||
onClick: () -> Unit = {},
|
||||
onClear: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(dimensionResource(R.dimen.padding_small)),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.weight(1F),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_server),
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = url,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = url,
|
||||
onClick = onClick,
|
||||
leading = {
|
||||
Icon(painter = painterResource(R.drawable.ic_server), contentDescription = null)
|
||||
},
|
||||
trailing = {
|
||||
TextButton(onClick = onClear) {
|
||||
Text(
|
||||
text = stringResource(R.string.remove),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
TextButton(onClick = onClear) {
|
||||
Text(
|
||||
text = stringResource(R.string.remove),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
||||
@@ -8,36 +8,15 @@ package com.aurora.store.compose.composable
|
||||
|
||||
import android.text.format.DateUtils
|
||||
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.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.PreviewParameter
|
||||
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.preview.AppPreviewProvider
|
||||
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.room.download.Download
|
||||
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
|
||||
fun DownloadListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
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)
|
||||
fun DownloadListItem(modifier: Modifier = Modifier, download: Download, onClick: () -> Unit = {}) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var isVisible by remember { mutableStateOf(true) }
|
||||
var isExpanded by remember { mutableStateOf(isExpanded) }
|
||||
|
||||
fun requestClear() {
|
||||
coroutineScope.launch {
|
||||
isVisible = false
|
||||
delay(300) // Let the animation play
|
||||
onClear()
|
||||
}
|
||||
val statusLine = stringResource(download.status.localized)
|
||||
val tertiaryLine = if (download.status in DownloadStatus.running) {
|
||||
val progress = "${download.progress}%"
|
||||
val speed = "${Formatter.formatShortFileSize(context, download.speed)}/s"
|
||||
val eta = getETAString(context, download.timeRemaining)
|
||||
"$progress • $speed • $eta"
|
||||
} else {
|
||||
DateUtils.formatDateTime(context, download.downloadedAt, DateUtils.FORMAT_SHOW_DATE)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandedMenu() {
|
||||
val context = LocalContext.current
|
||||
Box(modifier = modifier) {
|
||||
IconButton(onClick = { isExpanded = true }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_more_vert),
|
||||
contentDescription = stringResource(R.string.menu)
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = isExpanded, onDismissRequest = { isExpanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_cancel)) },
|
||||
onClick = onCancel,
|
||||
enabled = download.isRunning
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_install)) },
|
||||
onClick = onInstall,
|
||||
enabled = download.canInstall(context)
|
||||
)
|
||||
DropdownMenuItem(
|
||||
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
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = download.displayName,
|
||||
supporting = statusLine,
|
||||
tertiary = tertiaryLine,
|
||||
headlineStyle = MaterialTheme.typography.bodyMedium,
|
||||
onClick = onClick,
|
||||
leading = {
|
||||
AnimatedAppIcon(
|
||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
|
||||
iconUrl = download.iconURL,
|
||||
inProgress = download.isRunning,
|
||||
progress = download.progress.toFloat()
|
||||
)
|
||||
},
|
||||
trailing = when (download.status) {
|
||||
DownloadStatus.COMPLETED -> {
|
||||
{
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_check),
|
||||
contentDescription = stringResource(R.string.download_completed),
|
||||
tint = colorGreen
|
||||
)
|
||||
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)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
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
|
||||
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.painterResource
|
||||
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.PreviewParameter
|
||||
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.ThemePreviewProvider
|
||||
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
|
||||
fun FavouriteListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -64,35 +38,22 @@ fun FavouriteListItem(
|
||||
onClick: () -> Unit = {},
|
||||
onClear: () -> Unit = {}
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var isVisible by remember { mutableStateOf(true) }
|
||||
|
||||
fun requestClear() {
|
||||
coroutineScope.launch {
|
||||
isVisible = false
|
||||
delay(300) // Let the animation play
|
||||
onClear()
|
||||
}
|
||||
}
|
||||
|
||||
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_xsmall)
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(modifier = Modifier.weight(1F)) {
|
||||
val context = LocalContext.current
|
||||
RemovableListItem(onRemove = onClear) { triggerRemove ->
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = favourite.displayName,
|
||||
supporting = favourite.packageName,
|
||||
tertiary = DateUtils.formatDateTime(
|
||||
context,
|
||||
favourite.added,
|
||||
DateUtils.FORMAT_SHOW_DATE
|
||||
),
|
||||
headlineStyle = MaterialTheme.typography.bodyMedium,
|
||||
onClick = onClick,
|
||||
leading = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(favourite.iconURL)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
@@ -102,40 +63,16 @@ fun FavouriteListItem(
|
||||
.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 = 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
|
||||
},
|
||||
trailing = {
|
||||
IconButton(onClick = triggerRemove) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_favorite_checked),
|
||||
contentDescription = stringResource(R.string.action_favourite)
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { requestClear() }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_favorite_checked),
|
||||
contentDescription = stringResource(R.string.action_favourite)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,10 +57,10 @@ fun Info(
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null)
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_xxsmall)
|
||||
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||
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
|
||||
) {
|
||||
if (painter != null) Icon(painter = painter, contentDescription = null)
|
||||
|
||||
@@ -5,35 +5,16 @@
|
||||
|
||||
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.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.installer.SessionInstaller
|
||||
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
|
||||
fun InstallerListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -41,34 +22,14 @@ fun InstallerListItem(
|
||||
isSelected: Boolean = false,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(dimensionResource(R.dimen.padding_small)),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
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)
|
||||
}
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = stringResource(installerInfo.title),
|
||||
supporting = stringResource(installerInfo.subtitle),
|
||||
tertiary = stringResource(installerInfo.description),
|
||||
onClick = onClick,
|
||||
trailing = { RadioButton(selected = isSelected, onClick = onClick) }
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
|
||||
@@ -6,21 +6,9 @@
|
||||
|
||||
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.width
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.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 coil3.compose.AsyncImage
|
||||
@@ -38,12 +25,6 @@ import com.aurora.store.R
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
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
|
||||
fun LinkListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -51,46 +32,25 @@ fun LinkListItem(
|
||||
onClick: () -> Unit = {},
|
||||
iconTint: Color? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(dimensionResource(R.dimen.padding_small)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(link.icon)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_default))
|
||||
.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
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = link.title,
|
||||
supporting = link.subtitle,
|
||||
onClick = onClick,
|
||||
leading = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(link.icon)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_default))
|
||||
.clip(CircleShape),
|
||||
colorFilter = if (iconTint != null) ColorFilter.tint(color = iconTint) else null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
|
||||
@@ -5,32 +5,12 @@
|
||||
|
||||
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.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.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.aurora.store.R
|
||||
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
|
||||
fun LocaleListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -39,30 +19,15 @@ fun LocaleListItem(
|
||||
isChecked: Boolean = false,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { if (!isChecked) onClick() })
|
||||
.padding(dimensionResource(R.dimen.padding_small)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
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
|
||||
)
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = displayName,
|
||||
supporting = displayLanguage,
|
||||
onClick = { if (!isChecked) onClick() },
|
||||
trailing = {
|
||||
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
||||
}
|
||||
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
||||
@@ -36,7 +36,7 @@ fun Logo(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_small),
|
||||
dimensionResource(R.dimen.spacing_small),
|
||||
Alignment.CenterVertically
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
|
||||
@@ -91,13 +91,13 @@ fun MicroG(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_small),
|
||||
vertical = dimensionResource(R.dimen.padding_xxsmall)
|
||||
horizontal = dimensionResource(R.dimen.spacing_small),
|
||||
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||
),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.onboarding_gms_microg),
|
||||
@@ -116,8 +116,8 @@ fun MicroG(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = dimensionResource(R.dimen.padding_large)),
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
||||
.padding(vertical = dimensionResource(R.dimen.spacing_large)),
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||
) {
|
||||
uiState.items.fastForEach { item ->
|
||||
MicroGItemRow(item = item)
|
||||
@@ -127,7 +127,7 @@ fun MicroG(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.padding_small)
|
||||
dimensionResource(R.dimen.spacing_small)
|
||||
)
|
||||
) {
|
||||
Checkbox(
|
||||
@@ -218,8 +218,8 @@ private fun MicroGItemRow(item: ExternalItem) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_small),
|
||||
vertical = dimensionResource(R.dimen.padding_xsmall)
|
||||
horizontal = dimensionResource(R.dimen.spacing_small),
|
||||
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -232,7 +232,7 @@ private fun MicroGItemRow(item: ExternalItem) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
||||
.padding(horizontal = dimensionResource(R.dimen.spacing_small))
|
||||
) {
|
||||
Text(
|
||||
text = item.displayName,
|
||||
|
||||
@@ -43,7 +43,7 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_xsmall),
|
||||
dimensionResource(R.dimen.spacing_xsmall),
|
||||
Alignment.CenterHorizontally
|
||||
)
|
||||
) {
|
||||
@@ -60,7 +60,7 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I
|
||||
)
|
||||
val size by animateDpAsState(
|
||||
targetValue = if (isSelected) {
|
||||
dimensionResource(R.dimen.radius_normal)
|
||||
dimensionResource(R.dimen.radius_medium)
|
||||
} else {
|
||||
dimensionResource(R.dimen.radius_small)
|
||||
},
|
||||
|
||||
@@ -133,7 +133,7 @@ fun PermissionList(
|
||||
|
||||
LazyColumn(
|
||||
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)
|
||||
|
||||
|
||||
@@ -5,18 +5,10 @@
|
||||
|
||||
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.TextButton
|
||||
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
|
||||
@@ -26,49 +18,30 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.data.model.Permission
|
||||
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
|
||||
fun PermissionListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
permission: Permission,
|
||||
onAction: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dimensionResource(R.dimen.padding_small)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1F)) {
|
||||
Text(
|
||||
text = permission.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = permission.subtitle,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = permission.title,
|
||||
supporting = permission.subtitle,
|
||||
trailing = {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* 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.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.foundation.layout.size
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -29,27 +30,23 @@ import com.aurora.store.R
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
|
||||
/**
|
||||
* Composable to show error message that fills all available screen
|
||||
* @param modifier The modifier to be applied to the composable
|
||||
* @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
|
||||
* Full-screen empty/error placeholder: centered icon + message and an optional action button.
|
||||
* Replaces the previous EmptyState and Error composables.
|
||||
*/
|
||||
@Composable
|
||||
fun Error(
|
||||
fun Placeholder(
|
||||
modifier: Modifier = Modifier,
|
||||
painter: Painter,
|
||||
message: String,
|
||||
actionMessage: String? = null,
|
||||
actionLabel: String? = null,
|
||||
onAction: (() -> Unit)? = null
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
||||
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_small),
|
||||
dimensionResource(R.dimen.spacing_small),
|
||||
Alignment.CenterVertically
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
@@ -57,18 +54,18 @@ fun Error(
|
||||
Icon(
|
||||
painter = painter,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size))
|
||||
modifier = Modifier.size(dimensionResource(R.dimen.icon_size_medium)),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
if (actionMessage != null) {
|
||||
Button(onClick = { if (onAction != null) onAction() }, enabled = onAction != null) {
|
||||
if (actionLabel != null && onAction != null) {
|
||||
FilledTonalButton(onClick = onAction) {
|
||||
Text(
|
||||
text = actionMessage,
|
||||
text = actionLabel,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@@ -80,10 +77,11 @@ fun Error(
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun ErrorPreview() {
|
||||
Error(
|
||||
private fun PlaceholderPreview() {
|
||||
Placeholder(
|
||||
painter = painterResource(R.drawable.ic_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.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aurora.store.R
|
||||
@@ -48,7 +48,6 @@ import kotlinx.coroutines.launch
|
||||
fun ScrollHint(
|
||||
listState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
bottomPadding: Dp = 72.dp, // keep above bottom bar
|
||||
autoHideOnScroll: Boolean = true,
|
||||
enableBounce: Boolean = true,
|
||||
onClickScrollOffset: Float = 300f
|
||||
@@ -88,7 +87,7 @@ fun ScrollHint(
|
||||
visible = visible,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = bottomPadding),
|
||||
.padding(bottom = dimensionResource(R.dimen.scroll_hint_padding)),
|
||||
enter = fadeIn() + slideInVertically { it / 2 },
|
||||
exit = fadeOut()
|
||||
) {
|
||||
|
||||
@@ -5,24 +5,15 @@
|
||||
|
||||
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.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
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.PreviewWrapper
|
||||
import coil3.compose.AsyncImage
|
||||
@@ -32,13 +23,6 @@ import com.aurora.gplayapi.SearchSuggestEntry
|
||||
import com.aurora.store.R
|
||||
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
|
||||
fun SearchSuggestionListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -46,25 +30,11 @@ fun SearchSuggestionListItem(
|
||||
onClick: (query: String) -> Unit = {},
|
||||
onAction: (query: String) -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { onClick(searchSuggestEntry.title) })
|
||||
.padding(
|
||||
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))
|
||||
) {
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = searchSuggestEntry.title,
|
||||
onClick = { onClick(searchSuggestEntry.title) },
|
||||
leading = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(
|
||||
@@ -81,20 +51,16 @@ fun SearchSuggestionListItem(
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_default))
|
||||
)
|
||||
Text(
|
||||
text = searchSuggestEntry.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
IconButton(onClick = { onAction(searchSuggestEntry.title) }) {
|
||||
Icon(
|
||||
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)
|
||||
|
||||
@@ -12,15 +12,16 @@ 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.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.painterResource
|
||||
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
|
||||
@@ -28,34 +29,30 @@ import com.aurora.store.R
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
|
||||
/**
|
||||
* Composable to display sticky header in list
|
||||
* @param modifier The modifier to be applied to the composable
|
||||
* @param title Title to display
|
||||
* @param subtitle Optional subtitle to display
|
||||
* @param onClick Callback when this composable is clicked
|
||||
* @see TextDividerComposable
|
||||
* Section header row used throughout the app. Title with optional subtitle, an optional
|
||||
* trailing slot, and the whole row becomes clickable when [onClick] is non-null. When
|
||||
* [trailing] is null and [onClick] is set, a default right-chevron is shown.
|
||||
*/
|
||||
@Composable
|
||||
fun Header(
|
||||
fun SectionHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
onClick: (() -> Unit)? = null
|
||||
onClick: (() -> Unit)? = null,
|
||||
trailing: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null)
|
||||
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_xsmall)
|
||||
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||
vertical = dimensionResource(R.dimen.spacing_xsmall)
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1F)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
@@ -65,15 +62,20 @@ fun Header(
|
||||
if (!subtitle.isNullOrBlank()) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
if (onClick != null) {
|
||||
Icon(
|
||||
when {
|
||||
trailing != null -> trailing()
|
||||
onClick != null -> Icon(
|
||||
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)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun HeaderPreview() {
|
||||
Header(
|
||||
title = stringResource(R.string.details_privacy),
|
||||
subtitle = stringResource(R.string.exodus_powered),
|
||||
onClick = {}
|
||||
private fun SectionHeaderPreview() {
|
||||
SectionHeader(title = "Top Charts", 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(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dimensionResource(R.dimen.padding_small)),
|
||||
.padding(dimensionResource(R.dimen.spacing_small)),
|
||||
text = title.uppercase(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
|
||||
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.WindowInsets
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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 title Title of the screen
|
||||
* @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)
|
||||
*/
|
||||
@Composable
|
||||
@@ -33,15 +37,17 @@ fun TopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String? = null,
|
||||
navigationIcon: Painter = painterResource(R.drawable.ic_arrow_back),
|
||||
onNavigateUp: (() -> Unit)? = null,
|
||||
showNavigationIcon: Boolean = true,
|
||||
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
|
||||
actions: @Composable (RowScope.() -> Unit) = {}
|
||||
) {
|
||||
val activity = LocalActivity.current as? ComponentActivity
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
title = { if (title != null) Text(text = title) },
|
||||
navigationIcon = {
|
||||
if (onNavigateUp != null) {
|
||||
IconButton(onClick = onNavigateUp) {
|
||||
if (showNavigationIcon) {
|
||||
IconButton(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
painter = navigationIcon,
|
||||
contentDescription = stringResource(R.string.action_back)
|
||||
@@ -49,6 +55,7 @@ fun TopAppBar(
|
||||
}
|
||||
}
|
||||
},
|
||||
windowInsets = windowInsets,
|
||||
actions = actions
|
||||
)
|
||||
}
|
||||
@@ -58,7 +65,6 @@ fun TopAppBar(
|
||||
@Composable
|
||||
private fun TopAppBarPreview() {
|
||||
TopAppBar(
|
||||
title = stringResource(R.string.title_about),
|
||||
onNavigateUp = {}
|
||||
title = stringResource(R.string.title_about)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -111,7 +111,9 @@ private fun AnimatedAppIconPreview(@PreviewParameter(AppPreviewProvider::class)
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AnimatedAppIconPreview(@PreviewParameter(ProgressProvider::class) progress: Float) {
|
||||
private fun AnimatedAppIconInProgressPreview(
|
||||
@PreviewParameter(ProgressProvider::class) progress: Float
|
||||
) {
|
||||
AnimatedAppIcon(
|
||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
|
||||
iconUrl = "",
|
||||
|
||||
@@ -5,21 +5,24 @@
|
||||
|
||||
package com.aurora.store.compose.composable.app
|
||||
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.requiredSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
@@ -41,38 +44,38 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
*/
|
||||
@Composable
|
||||
fun AppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(dimensionResource(R.dimen.icon_size_cluster))
|
||||
.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(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(app.iconArtwork.url)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = ContentScale.Crop
|
||||
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_cluster))
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
||||
)
|
||||
Text(
|
||||
.fillMaxWidth(),
|
||||
text = app.displayName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = if (app.size > 0) {
|
||||
Formatter.formatShortFileSize(context, app.size)
|
||||
} else {
|
||||
app.downloadString
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.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.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
@@ -31,79 +24,49 @@ import coil3.request.crossfade
|
||||
import com.aurora.extensions.requiresGMS
|
||||
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
|
||||
|
||||
/**
|
||||
* 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
|
||||
fun LargeAppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) {
|
||||
@Composable
|
||||
fun buildExtras(app: App): List<String> = mutableListOf<String>().apply {
|
||||
add(if (app.size > 0) CommonUtil.addSiPrefix(app.size) else app.downloadString)
|
||||
add("${app.labeledRating}★")
|
||||
|
||||
if (app.isFree) {
|
||||
add(stringResource(R.string.details_free))
|
||||
} else {
|
||||
add(stringResource(R.string.details_paid))
|
||||
}
|
||||
|
||||
if (app.containsAds) {
|
||||
add(stringResource(R.string.details_contains_ads))
|
||||
} else {
|
||||
add(stringResource(R.string.details_no_ads))
|
||||
}
|
||||
|
||||
if (app.requiresGMS()) {
|
||||
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
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = app.displayName,
|
||||
supporting = app.developerName,
|
||||
tertiary = buildAppExtras(app),
|
||||
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 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)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
|
||||
@@ -38,7 +38,7 @@ fun ExodusListItem(modifier: Modifier = Modifier, tracker: ExodusTracker) {
|
||||
onClick = { context.browse(tracker.url) },
|
||||
enabled = tracker.url.isNotBlank()
|
||||
)
|
||||
.padding(dimensionResource(R.dimen.padding_small))
|
||||
.padding(dimensionResource(R.dimen.spacing_small))
|
||||
) {
|
||||
Text(
|
||||
text = tracker.name,
|
||||
|
||||
@@ -34,9 +34,9 @@ fun RatingListItem(modifier: Modifier = Modifier, label: String, rating: Float)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dimensionResource(R.dimen.padding_small)),
|
||||
.padding(horizontal = dimensionResource(R.dimen.spacing_small)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
|
||||
@@ -45,8 +45,8 @@ fun ReviewListItem(modifier: Modifier = Modifier, review: Review) {
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||
vertical = dimensionResource(R.dimen.spacing_small)
|
||||
)
|
||||
) {
|
||||
AsyncImage(
|
||||
@@ -61,7 +61,7 @@ fun ReviewListItem(modifier: Modifier = Modifier, review: Review) {
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_small))
|
||||
) {
|
||||
Text(
|
||||
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
|
||||
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation.NavDeepLinkBuilder
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.metadata
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
@@ -24,11 +31,13 @@ import com.aurora.Constants.PACKAGE_NAME_GMS
|
||||
import com.aurora.extensions.toast
|
||||
import com.aurora.store.AuroraApp
|
||||
import com.aurora.store.ComposeActivity
|
||||
import com.aurora.store.MainActivity
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.ui.about.AboutScreen
|
||||
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.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.StreamBrowseScreen
|
||||
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.favourite.FavouriteScreen
|
||||
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.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.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.splash.SplashScreen
|
||||
import com.aurora.store.compose.ui.spoof.SpoofScreen
|
||||
import com.aurora.store.data.event.InstallerEvent
|
||||
import com.aurora.store.data.model.AccountType
|
||||
@@ -54,23 +71,6 @@ import com.aurora.store.util.Preferences
|
||||
@Composable
|
||||
fun NavDisplay(startDestination: NavKey) {
|
||||
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
|
||||
|
||||
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(
|
||||
onBack = { backstack.removeLastOrNull() },
|
||||
backStack = backstack,
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
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 {
|
||||
entry<Screen.Blacklist> {
|
||||
BlacklistScreen(onNavigateUp = ::onNavigateUp)
|
||||
}
|
||||
|
||||
entry<Screen.Search> {
|
||||
SearchScreen(onNavigateUp = ::onNavigateUp)
|
||||
entry<Screen.Main> { screen ->
|
||||
MainScreen(
|
||||
initialTab = screen.initialTab,
|
||||
onNavigateTo = ::navigate
|
||||
)
|
||||
}
|
||||
|
||||
entry<Screen.AppDetails> { screen ->
|
||||
AppDetailsScreen(
|
||||
packageName = screen.packageName,
|
||||
onNavigateUp = ::onNavigateUp,
|
||||
onNavigateToAppDetails = { packageName ->
|
||||
backstack.add(Screen.AppDetails(packageName))
|
||||
}
|
||||
onNavigateTo = ::navigate
|
||||
)
|
||||
}
|
||||
|
||||
entry<Screen.DevProfile> { screen ->
|
||||
DevProfileScreen(
|
||||
developerId = screen.developerId,
|
||||
onNavigateUp = ::onNavigateUp,
|
||||
onNavigateToAppDetails = { packageName ->
|
||||
backstack.add(Screen.AppDetails(packageName))
|
||||
}
|
||||
onNavigateTo = ::navigate
|
||||
)
|
||||
}
|
||||
|
||||
entry<Screen.PermissionRationale> { screen ->
|
||||
PermissionRationaleScreen(
|
||||
onNavigateUp = ::onNavigateUp,
|
||||
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 ->
|
||||
StreamBrowseScreen(
|
||||
streamCluster = screen.streamCluster,
|
||||
onNavigateUp = ::onNavigateUp,
|
||||
onNavigateToAppDetails = { packageName ->
|
||||
backstack.add(Screen.AppDetails(packageName))
|
||||
}
|
||||
onNavigateTo = ::navigate
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
data object Accounts : Screen()
|
||||
|
||||
@Serializable
|
||||
data object GoogleLogin : Screen()
|
||||
|
||||
@Serializable
|
||||
data object About : Screen()
|
||||
|
||||
@@ -67,4 +70,34 @@ sealed class Screen : NavKey, Parcelable {
|
||||
|
||||
@Serializable
|
||||
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 successColor = Color(0xFF1B8738) // Green
|
||||
|
||||
val colorGreen = Color(0xFF4CAF50)
|
||||
val colorRed = Color(0xFFF44336)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package com.aurora.store.compose.theme
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
@@ -16,7 +17,12 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
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.LocalView
|
||||
import androidx.compose.ui.res.colorResource
|
||||
@@ -31,7 +37,19 @@ import com.aurora.store.util.Preferences
|
||||
fun AuroraTheme(content: @Composable () -> Unit) {
|
||||
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 lightScheme = if (isDynamicColorSupported) {
|
||||
@@ -69,11 +87,9 @@ fun AuroraTheme(content: @Composable () -> Unit) {
|
||||
val currentActivity = activity ?: return@SideEffect
|
||||
val window = currentActivity.window
|
||||
|
||||
// Transparent system bars
|
||||
window.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
||||
|
||||
// Control icon colors explicitly
|
||||
WindowCompat
|
||||
.getInsetsController(window, view)
|
||||
.apply {
|
||||
|
||||
@@ -46,7 +46,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.data.model.Link
|
||||
|
||||
@Composable
|
||||
fun AboutScreen(onNavigateUp: () -> Unit) {
|
||||
fun AboutScreen() {
|
||||
var shouldShowAboutDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (shouldShowAboutDialog) {
|
||||
@@ -54,13 +54,12 @@ fun AboutScreen(onNavigateUp: () -> Unit) {
|
||||
}
|
||||
|
||||
ScreenContent(
|
||||
onNavigateUp = onNavigateUp,
|
||||
onAboutAurora = { shouldShowAboutDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(onNavigateUp: () -> Unit = {}, onAboutAurora: () -> Unit = {}) {
|
||||
private fun ScreenContent(onAboutAurora: () -> Unit = {}) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val linkURLS = stringArrayResource(R.array.link_urls)
|
||||
@@ -94,8 +93,7 @@ private fun ScreenContent(onNavigateUp: () -> Unit = {}, onAboutAurora: () -> Un
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(R.string.title_about),
|
||||
onNavigateUp = onNavigateUp
|
||||
title = stringResource(R.string.title_about)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -103,7 +101,7 @@ private fun ScreenContent(onNavigateUp: () -> Unit = {}, onAboutAurora: () -> Un
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||
) {
|
||||
stickyHeader {
|
||||
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||
@@ -130,9 +128,9 @@ private fun BrandHeader() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dimensionResource(R.dimen.margin_medium)),
|
||||
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
|
||||
@@ -48,19 +48,18 @@ import com.aurora.Constants.URL_TOS
|
||||
import com.aurora.extensions.browse
|
||||
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.providers.AccountProvider
|
||||
import com.aurora.store.viewmodel.accounts.AccountsViewModel
|
||||
|
||||
@Composable
|
||||
fun AccountsScreen(
|
||||
onNavigateUp: () -> Unit,
|
||||
onNavigateToSplash: () -> Unit,
|
||||
onNavigateTo: (Destination) -> Unit,
|
||||
viewModel: AccountsViewModel = hiltViewModel()
|
||||
) {
|
||||
val userProfile = viewModel.authProvider.authData?.userProfile
|
||||
val avatar = when {
|
||||
viewModel.authProvider.isAnonymous -> R.mipmap.ic_launcher
|
||||
userProfile != null -> userProfile.artwork.url
|
||||
else -> R.mipmap.ic_launcher
|
||||
}
|
||||
@@ -70,7 +69,6 @@ fun AccountsScreen(
|
||||
else -> stringResource(R.string.account_anonymous)
|
||||
}
|
||||
val email = when {
|
||||
viewModel.authProvider.isAnonymous -> stringResource(R.string.account_anonymous_email)
|
||||
userProfile != null -> userProfile.email
|
||||
else -> stringResource(R.string.account_anonymous_email)
|
||||
}
|
||||
@@ -83,14 +81,13 @@ fun AccountsScreen(
|
||||
onConfirm = {
|
||||
shouldShowLogoutDialog = false
|
||||
AccountProvider.logout(context)
|
||||
onNavigateToSplash()
|
||||
onNavigateTo(Destination.Splash)
|
||||
},
|
||||
onDismiss = { shouldShowLogoutDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
ScreenContent(
|
||||
onNavigateUp = onNavigateUp,
|
||||
avatar = avatar,
|
||||
name = name,
|
||||
email = email,
|
||||
@@ -100,17 +97,15 @@ fun AccountsScreen(
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
avatar: Any = R.mipmap.ic_launcher,
|
||||
avatar: Any = R.drawable.ic_account,
|
||||
name: String = stringResource(R.string.account_anonymous),
|
||||
email: String = stringResource(R.string.account_anonymous_email),
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onLogout: () -> Unit = {}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(R.string.title_account_manager),
|
||||
onNavigateUp = onNavigateUp
|
||||
title = stringResource(R.string.title_account_manager)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -118,7 +113,7 @@ private fun ScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(vertical = dimensionResource(R.dimen.padding_medium)),
|
||||
.padding(vertical = dimensionResource(R.dimen.spacing_small)),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
AssistHeader()
|
||||
@@ -126,7 +121,7 @@ private fun ScreenContent(
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
@@ -158,7 +153,9 @@ private fun ScreenContent(
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = dimensionResource(R.dimen.height_bottom_adj)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.spacing_medium)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.account_logout),
|
||||
@@ -171,7 +168,7 @@ private fun ScreenContent(
|
||||
painter = painterResource(R.drawable.ic_logout),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_xsmall)))
|
||||
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing_xsmall)))
|
||||
Text(
|
||||
text = stringResource(R.string.action_logout)
|
||||
)
|
||||
@@ -193,8 +190,8 @@ private fun AssistHeader() {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal))
|
||||
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
items(items = links.keys.toList(), key = { item -> item }) { label ->
|
||||
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
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -36,8 +38,10 @@ import androidx.compose.material3.rememberSearchBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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.ui.blacklist.menu.BlacklistMenu
|
||||
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.viewmodel.blacklist.BlacklistViewModel
|
||||
import java.util.Calendar
|
||||
@@ -72,13 +77,16 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hiltViewModel()) {
|
||||
fun BlacklistScreen(viewModel: BlacklistViewModel = hiltViewModel()) {
|
||||
val context = LocalContext.current
|
||||
val packages by viewModel.filteredPackages.collectAsStateWithLifecycle()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val installers by viewModel.installers.collectAsStateWithLifecycle()
|
||||
|
||||
ScreenContent(
|
||||
packages = packages,
|
||||
onNavigateUp = onNavigateUp,
|
||||
sortFilterState = state,
|
||||
installers = installers,
|
||||
isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist },
|
||||
onBlacklistImport = { uri ->
|
||||
viewModel.importBlacklist(uri)
|
||||
@@ -92,14 +100,16 @@ fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hi
|
||||
onBlacklistAll = { viewModel.blacklistAll() },
|
||||
onWhitelist = { packageName -> viewModel.whitelist(packageName) },
|
||||
onWhitelistAll = { viewModel.whitelistAll() },
|
||||
onSearch = { query -> viewModel.search(query) }
|
||||
onSearch = { query -> viewModel.search(query) },
|
||||
onSortFilterStateChange = viewModel::updateState
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
packages: List<BlacklistAppItem>? = null,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
sortFilterState: SortFilterState = SortFilterState(),
|
||||
installers: Map<String, String> = emptyMap(),
|
||||
isPackageBlacklisted: (packageName: String) -> Boolean = { false },
|
||||
onBlacklistImport: (uri: Uri) -> Unit = {},
|
||||
onBlacklistExport: (uri: Uri) -> Unit = {},
|
||||
@@ -107,13 +117,16 @@ private fun ScreenContent(
|
||||
onBlacklistAll: () -> Unit = {},
|
||||
onWhitelist: (packageName: String) -> 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 textFieldState = rememberTextFieldState()
|
||||
val searchBarState = rememberSearchBarState()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var sheetVisible by remember { mutableStateOf(false) }
|
||||
|
||||
val docImportLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
@@ -224,14 +237,22 @@ private fun ScreenContent(
|
||||
state = searchBarState,
|
||||
inputField = inputField,
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateUp) {
|
||||
IconButton(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_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(
|
||||
appBarContainerColor = Color.Transparent
|
||||
)
|
||||
@@ -248,8 +269,8 @@ private fun ScreenContent(
|
||||
.fillMaxWidth()
|
||||
.clickable { onRequestSearch(pkg.displayName) }
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||
vertical = dimensionResource(R.dimen.spacing_small)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -298,7 +319,7 @@ private fun ScreenContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_xxsmall)
|
||||
dimensionResource(R.dimen.spacing_xsmall)
|
||||
)
|
||||
) {
|
||||
if (selectedPackages.isNotEmpty()) {
|
||||
@@ -333,12 +354,20 @@ private fun ScreenContent(
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sheetVisible) {
|
||||
SortFilterSheet(
|
||||
state = sortFilterState,
|
||||
installers = installers,
|
||||
onStateChange = onSortFilterStateChange,
|
||||
onDismiss = { sheetVisible = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
fun PermissionRationaleScreen(
|
||||
requiredPermissions: Set<PermissionType> = emptySet(),
|
||||
onNavigateUp: () -> Unit,
|
||||
onPermissionCallback: (type: PermissionType) -> Unit = {},
|
||||
viewModel: PermissionRationaleViewModel = hiltViewModel()
|
||||
) {
|
||||
val permissions by viewModel.permissions.collectAsStateWithLifecycle()
|
||||
|
||||
ScreenContent(
|
||||
onNavigateUp = onNavigateUp,
|
||||
permissions = permissions
|
||||
.filter { it.type in requiredPermissions }
|
||||
.map { permission -> permission.copy(optional = false) },
|
||||
@@ -56,7 +54,6 @@ fun PermissionRationaleScreen(
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
permissions: List<Permission> = emptyList(),
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onPermissionCallback: (type: PermissionType) -> Unit = {},
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
@@ -64,8 +61,7 @@ private fun ScreenContent(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = pluralStringResource(R.plurals.permissions_required, permissions.size),
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
}
|
||||
) { 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
|
||||
|
||||
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.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
|
||||
@@ -26,9 +28,10 @@ import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.gplayapi.data.models.StreamCluster
|
||||
import com.aurora.store.R
|
||||
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.app.LargeAppListItem
|
||||
import com.aurora.store.compose.navigation.Destination
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.viewmodel.browse.StreamBrowseViewModel
|
||||
@@ -39,8 +42,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@Composable
|
||||
fun StreamBrowseScreen(
|
||||
streamCluster: StreamCluster,
|
||||
onNavigateUp: () -> Unit,
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||
onNavigateTo: (Destination) -> Unit,
|
||||
viewModel: StreamBrowseViewModel = hiltViewModel(
|
||||
creationCallback = { factory: StreamBrowseViewModel.Factory ->
|
||||
factory.create(streamCluster)
|
||||
@@ -52,8 +54,7 @@ fun StreamBrowseScreen(
|
||||
ScreenContent(
|
||||
title = streamCluster.clusterTitle,
|
||||
apps = apps,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onNavigateToAppDetails = onNavigateToAppDetails
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,19 +62,18 @@ fun StreamBrowseScreen(
|
||||
private fun ScreenContent(
|
||||
title: String = String(),
|
||||
apps: LazyPagingItems<App> = emptyPagingItems(),
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
||||
onNavigateUp: () -> Unit = {}
|
||||
onNavigateTo: (Destination) -> Unit = {}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(title = title, onNavigateUp = onNavigateUp)
|
||||
TopAppBar(title = title)
|
||||
}
|
||||
) { paddingValues ->
|
||||
when (apps.loadState.refresh) {
|
||||
is LoadState.Loading -> ContainedLoadingIndicator()
|
||||
|
||||
is LoadState.Error -> {
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_disclaimer),
|
||||
message = stringResource(R.string.error)
|
||||
@@ -82,7 +82,7 @@ private fun ScreenContent(
|
||||
|
||||
else -> {
|
||||
if (apps.itemCount == 0) {
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_disclaimer),
|
||||
message = stringResource(R.string.no_apps_available)
|
||||
@@ -91,7 +91,10 @@ private fun ScreenContent(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(paddingValues),
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.spacing_medium)
|
||||
)
|
||||
) {
|
||||
items(
|
||||
count = apps.itemCount,
|
||||
@@ -100,7 +103,9 @@ private fun ScreenContent(
|
||||
apps[index]?.let { app ->
|
||||
LargeAppListItem(
|
||||
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.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||
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.PreviewScreenSizes
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -63,15 +59,21 @@ import com.aurora.extensions.share
|
||||
import com.aurora.extensions.toast
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
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.store.ComposeActivity
|
||||
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.Error
|
||||
import com.aurora.store.compose.composable.Header
|
||||
import com.aurora.store.compose.composable.Placeholder
|
||||
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.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.preview.AppPreviewProvider
|
||||
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.navigation.ExtraScreen
|
||||
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.model.AppState
|
||||
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.viewmodel.details.AppDetailsViewModel
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||
import kotlin.random.Random
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AppDetailsScreen(
|
||||
packageName: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||
onNavigateTo: (Destination) -> Unit,
|
||||
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||
forceSinglePane: Boolean = false
|
||||
) {
|
||||
@@ -124,10 +125,22 @@ fun AppDetailsScreen(
|
||||
val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle()
|
||||
val dataSafetyReport by viewModel.dataSafetyReport.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) }
|
||||
|
||||
app?.let { loadedApp ->
|
||||
installError?.let { err ->
|
||||
InstallErrorSheet(
|
||||
app = loadedApp,
|
||||
error = err.error,
|
||||
extra = err.extra,
|
||||
onDismiss = viewModel::dismissInstallError
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
contentKey = { stateKey(it, app) },
|
||||
@@ -136,11 +149,10 @@ fun AppDetailsScreen(
|
||||
) { currentState ->
|
||||
when {
|
||||
currentState is AppState.Loading || app == null ->
|
||||
ScreenContentLoading(onNavigateUp = onNavigateUp)
|
||||
ScreenContentLoading()
|
||||
|
||||
currentState is AppState.Error ->
|
||||
ScreenContentError(
|
||||
onNavigateUp = onNavigateUp,
|
||||
message = currentState.message
|
||||
)
|
||||
|
||||
@@ -149,15 +161,15 @@ fun AppDetailsScreen(
|
||||
ScreenContentApp(
|
||||
app = loadedApp,
|
||||
featuredReviews = featuredReviews,
|
||||
suggestions = suggestions,
|
||||
suggestionsBundle = suggestionsBundle,
|
||||
isFavorite = favorite,
|
||||
isAnonymous = viewModel.authProvider.isAnonymous,
|
||||
state = currentState,
|
||||
plexusScores = plexusScores,
|
||||
dataSafetyReport = dataSafetyReport,
|
||||
exodusReport = exodusReport,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onNavigateToAppDetails = onNavigateToAppDetails,
|
||||
onNavigateTo = onNavigateTo,
|
||||
onLoadMoreCluster = { cluster -> viewModel.loadMoreCluster(cluster) },
|
||||
onDownload = { requestedApp -> viewModel.enqueueDownload(requestedApp) },
|
||||
onFavorite = { viewModel.toggleFavourite(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
|
||||
private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) {
|
||||
private fun ScreenContentLoading() {
|
||||
Scaffold(
|
||||
topBar = { TopAppBar(onNavigateUp = onNavigateUp) }
|
||||
topBar = { TopAppBar() }
|
||||
) { 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
|
||||
private fun ScreenContentError(onNavigateUp: () -> Unit = {}, message: String? = null) {
|
||||
private fun ScreenContentError(message: String? = null) {
|
||||
Scaffold(
|
||||
topBar = { TopAppBar(onNavigateUp = onNavigateUp) }
|
||||
topBar = { TopAppBar() }
|
||||
) { paddingValues ->
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_apps_outage),
|
||||
message = message ?: stringResource(R.string.toast_app_unavailable)
|
||||
@@ -227,15 +239,15 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}, message: String? =
|
||||
private fun ScreenContentApp(
|
||||
app: App,
|
||||
featuredReviews: List<Review> = emptyList(),
|
||||
suggestions: List<App> = emptyList(),
|
||||
suggestionsBundle: StreamBundle? = StreamBundle.EMPTY,
|
||||
isFavorite: Boolean = false,
|
||||
isAnonymous: Boolean = true,
|
||||
state: AppState = AppState.Unavailable,
|
||||
plexusScores: Scores? = null,
|
||||
dataSafetyReport: DataSafetyReport? = null,
|
||||
exodusReport: Report? = null,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
||||
onNavigateTo: (Destination) -> Unit = {},
|
||||
onLoadMoreCluster: (cluster: StreamCluster) -> Unit = {},
|
||||
onDownload: (requestedApp: App) -> Unit = {},
|
||||
onFavorite: () -> Unit = {},
|
||||
onCancelDownload: () -> Unit = {},
|
||||
@@ -412,7 +424,6 @@ private fun ScreenContentApp(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
onNavigateUp = onNavigateUp,
|
||||
actions = { if (shouldShowMenuOnMainPane) SetupMenu() }
|
||||
)
|
||||
}
|
||||
@@ -427,7 +438,7 @@ private fun ScreenContentApp(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_medium)
|
||||
dimensionResource(R.dimen.spacing_medium)
|
||||
),
|
||||
state = listState
|
||||
) {
|
||||
@@ -452,7 +463,7 @@ private fun ScreenContentApp(
|
||||
}
|
||||
|
||||
item {
|
||||
Header(
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.details_more_about_app),
|
||||
subtitle = app.shortDescription,
|
||||
onClick = { showExtraPane(ExtraScreen.More) }
|
||||
@@ -488,7 +499,7 @@ private fun ScreenContentApp(
|
||||
}
|
||||
|
||||
item {
|
||||
Header(
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.details_permission),
|
||||
subtitle = if (app.permissions.isNotEmpty()) {
|
||||
stringResource(R.string.permissions_requested, app.permissions.size)
|
||||
@@ -532,10 +543,17 @@ private fun ScreenContentApp(
|
||||
email = app.developerEmail
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldShowMenuOnMainPane) {
|
||||
suggestionClusterItems(
|
||||
suggestionsBundle = suggestionsBundle,
|
||||
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
||||
onClusterScrolled = onLoadMoreCluster
|
||||
)
|
||||
}
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
@@ -546,38 +564,22 @@ private fun ScreenContentApp(
|
||||
fun SupportingPane() {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(actions = { if (!shouldShowMenuOnMainPane) SetupMenu() })
|
||||
TopAppBar(
|
||||
showNavigationIcon = false,
|
||||
actions = { if (!shouldShowMenuOnMainPane) SetupMenu() }
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (suggestions.isNotEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.margin_medium)),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val hasContent = suggestionsBundle == null ||
|
||||
suggestionsBundle.streamClusters.isNotEmpty()
|
||||
if (hasContent) {
|
||||
StreamCarousel(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
streamBundle = suggestionsBundle,
|
||||
filterSingleAppClusters = false,
|
||||
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
||||
onClusterScrolled = onLoadMoreCluster
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -585,52 +587,43 @@ private fun ScreenContentApp(
|
||||
@Composable
|
||||
fun ExtraPane(screen: NavKey) = when (screen) {
|
||||
is ExtraScreen.Review -> ReviewScreen(
|
||||
packageName = app.packageName,
|
||||
onNavigateUp = ::onNavigateBack
|
||||
packageName = app.packageName
|
||||
)
|
||||
|
||||
is ExtraScreen.Exodus -> ExodusScreen(
|
||||
packageName = app.packageName,
|
||||
onNavigateUp = ::onNavigateBack
|
||||
packageName = app.packageName
|
||||
)
|
||||
|
||||
is ExtraScreen.More -> MoreScreen(
|
||||
packageName = app.packageName,
|
||||
onNavigateUp = ::onNavigateBack,
|
||||
onNavigateToAppDetails = onNavigateToAppDetails
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
|
||||
is ExtraScreen.Permission -> PermissionScreen(
|
||||
packageName = app.packageName,
|
||||
onNavigateUp = ::onNavigateBack
|
||||
packageName = app.packageName
|
||||
)
|
||||
|
||||
is ExtraScreen.Screenshot -> ScreenshotScreen(
|
||||
packageName = app.packageName,
|
||||
index = screen.index,
|
||||
onNavigateUp = ::onNavigateBack
|
||||
index = screen.index
|
||||
)
|
||||
|
||||
is ExtraScreen.ManualDownload -> ManualDownloadScreen(
|
||||
packageName = app.packageName,
|
||||
onNavigateUp = ::onNavigateBack,
|
||||
onRequestInstall = { requestedApp -> onInstall(requestedApp) }
|
||||
)
|
||||
|
||||
is ExtraScreen.MicroG -> MicroGScreen(
|
||||
packageName = app.packageName,
|
||||
onNavigateUp = ::onNavigateBack,
|
||||
onProceed = { onInstall(ignoreMicroG = true) }
|
||||
)
|
||||
|
||||
is Screen.DevProfile -> DevProfileScreen(
|
||||
publisherId = app.developerName,
|
||||
onNavigateUp = ::onNavigateBack,
|
||||
onNavigateToAppDetails = { onNavigateToAppDetails(it) }
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
|
||||
is Screen.PermissionRationale -> PermissionRationaleScreen(
|
||||
onNavigateUp = ::onNavigateBack,
|
||||
requiredPermissions = screen.requiredPermissions,
|
||||
onPermissionCallback = { type ->
|
||||
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)
|
||||
@PreviewScreenSizes
|
||||
@Composable
|
||||
@@ -665,7 +690,20 @@ private fun AppDetailsScreenPreview(@PreviewParameter(AppPreviewProvider::class)
|
||||
ScreenContentApp(
|
||||
app = app,
|
||||
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.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.aurora.Constants.EXODUS_REPORT_URL
|
||||
@@ -41,9 +40,9 @@ import com.aurora.extensions.browse
|
||||
import com.aurora.extensions.isWindowCompact
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.composable.Error
|
||||
import com.aurora.store.compose.composable.Header
|
||||
import com.aurora.store.compose.composable.Placeholder
|
||||
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.details.ExodusListItem
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
@@ -56,7 +55,6 @@ import com.aurora.store.viewmodel.details.ExodusViewModel
|
||||
@Composable
|
||||
fun ExodusScreen(
|
||||
packageName: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||
exodusViewModel: ExodusViewModel = hiltViewModel(key = "$packageName/exodus"),
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
@@ -80,7 +78,6 @@ fun ExodusScreen(
|
||||
null -> {
|
||||
ScreenContentError(
|
||||
topAppBarTitle = topAppBarTitle,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") }
|
||||
)
|
||||
}
|
||||
@@ -90,7 +87,6 @@ fun ExodusScreen(
|
||||
topAppBarTitle = topAppBarTitle,
|
||||
report = report,
|
||||
trackers = trackers,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") }
|
||||
)
|
||||
}
|
||||
@@ -102,7 +98,6 @@ private fun ScreenContentReport(
|
||||
topAppBarTitle: String? = null,
|
||||
report: Report? = null,
|
||||
trackers: List<ExodusTracker> = emptyList(),
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onRequestAnalysis: () -> Unit = {},
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
@@ -112,8 +107,7 @@ private fun ScreenContentReport(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = topAppBarTitle,
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
@@ -134,12 +128,12 @@ private fun ScreenContentReport(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
state = listState
|
||||
) {
|
||||
stickyHeader {
|
||||
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||
Header(
|
||||
SectionHeader(
|
||||
title = if (report?.trackers.isNullOrEmpty()) {
|
||||
stringResource(R.string.exodus_no_tracker)
|
||||
} else {
|
||||
@@ -161,7 +155,6 @@ private fun ScreenContentReport(
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
@@ -174,7 +167,6 @@ private fun ScreenContentReport(
|
||||
@Composable
|
||||
private fun ScreenContentError(
|
||||
topAppBarTitle: String? = null,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onRequestAnalysis: () -> Unit = {},
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
@@ -182,16 +174,15 @@ private fun ScreenContentError(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = topAppBarTitle,
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_disclaimer),
|
||||
message = stringResource(R.string.failed_to_fetch_report),
|
||||
actionMessage = stringResource(R.string.action_request_analysis),
|
||||
actionLabel = stringResource(R.string.action_request_analysis),
|
||||
onAction = onRequestAnalysis
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -69,7 +71,6 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun ManualDownloadScreen(
|
||||
packageName: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
onRequestInstall: (requestedApp: App) -> Unit,
|
||||
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
@@ -85,7 +86,6 @@ fun ManualDownloadScreen(
|
||||
state = state,
|
||||
topAppBarTitle = topAppBarTitle,
|
||||
currentVersionCode = app!!.versionCode,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onRequestInstall = { versionCode ->
|
||||
val requestedApp = app!!.copy(
|
||||
versionCode = versionCode,
|
||||
@@ -105,10 +105,10 @@ private fun ScreenContent(
|
||||
state: AppState = AppState.Unavailable,
|
||||
topAppBarTitle: String? = null,
|
||||
currentVersionCode: Long = 0L,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onRequestInstall: (versionCode: Long) -> Unit = {},
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
val activity = LocalActivity.current as? ComponentActivity
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
val errorMessage = stringResource(R.string.manual_download_version_error)
|
||||
@@ -130,8 +130,7 @@ private fun ScreenContent(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = topAppBarTitle,
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackBarHostState) }
|
||||
@@ -140,12 +139,14 @@ private fun ScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
||||
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.spacing_medium)
|
||||
)
|
||||
) {
|
||||
Info(
|
||||
painter = painterResource(R.drawable.ic_download_manager),
|
||||
@@ -182,12 +183,12 @@ private fun ScreenContent(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.padding_medium)
|
||||
dimensionResource(R.dimen.spacing_medium)
|
||||
)
|
||||
) {
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.weight(1F),
|
||||
onClick = onNavigateUp
|
||||
onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_close),
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -55,7 +57,6 @@ import com.aurora.store.viewmodel.onboarding.MicroGViewModel
|
||||
@Composable
|
||||
fun MicroGScreen(
|
||||
packageName: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
onProceed: () -> Unit,
|
||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||
viewModel: MicroGViewModel = hiltViewModel(),
|
||||
@@ -70,7 +71,6 @@ fun MicroGScreen(
|
||||
ScreenContent(
|
||||
topAppBarTitle = topAppBarTitle,
|
||||
uiState = viewModel.uiState,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onInstall = { viewModel.downloadMicroG() },
|
||||
onRetry = { viewModel.retryDownload() },
|
||||
onProceed = onProceed
|
||||
@@ -81,12 +81,12 @@ fun MicroGScreen(
|
||||
private fun ScreenContent(
|
||||
topAppBarTitle: String? = null,
|
||||
uiState: MicroGUIState = MicroGUIState(),
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onInstall: () -> Unit = {},
|
||||
onRetry: () -> Unit = {},
|
||||
onProceed: () -> Unit = {},
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
val activity = LocalActivity.current as? ComponentActivity
|
||||
val context = LocalContext.current
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
@@ -94,8 +94,7 @@ private fun ScreenContent(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = topAppBarTitle,
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@@ -104,15 +103,15 @@ private fun ScreenContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium))
|
||||
.padding(vertical = dimensionResource(R.dimen.padding_small)),
|
||||
.padding(horizontal = dimensionResource(R.dimen.spacing_medium))
|
||||
.padding(vertical = dimensionResource(R.dimen.spacing_small)),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.padding_medium)
|
||||
dimensionResource(R.dimen.spacing_medium)
|
||||
)
|
||||
) {
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.weight(1F),
|
||||
onClick = onNavigateUp
|
||||
onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_cancel),
|
||||
@@ -148,7 +147,7 @@ private fun ScreenContent(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
state = listState
|
||||
) {
|
||||
item {
|
||||
@@ -174,7 +173,6 @@ private fun ScreenContent(
|
||||
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
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.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.aurora.extensions.adaptiveNavigationIcon
|
||||
import com.aurora.extensions.isWindowCompact
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
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.ScrollHint
|
||||
import com.aurora.store.compose.composable.SectionHeader
|
||||
import com.aurora.store.compose.composable.TopAppBar
|
||||
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.ThemePreviewProvider
|
||||
import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
||||
@@ -52,8 +52,7 @@ import com.aurora.store.viewmodel.details.MoreViewModel
|
||||
@Composable
|
||||
fun MoreScreen(
|
||||
packageName: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||
onNavigateTo: (Destination) -> Unit,
|
||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||
moreViewModel: MoreViewModel = hiltViewModel(
|
||||
key = "$packageName/more",
|
||||
@@ -68,8 +67,7 @@ fun MoreScreen(
|
||||
ScreenContent(
|
||||
app = app!!,
|
||||
dependencies = dependencies,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onNavigateToAppDetails = onNavigateToAppDetails
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,8 +75,7 @@ fun MoreScreen(
|
||||
private fun ScreenContent(
|
||||
app: App,
|
||||
dependencies: List<App>? = null,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
||||
onNavigateTo: (Destination) -> Unit = {},
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
val topAppBarTitle = when {
|
||||
@@ -90,8 +87,7 @@ private fun ScreenContent(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = topAppBarTitle,
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -104,16 +100,18 @@ private fun ScreenContent(
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.spacing_medium)
|
||||
)
|
||||
) {
|
||||
item {
|
||||
Header(title = stringResource(R.string.details_description))
|
||||
SectionHeader(title = stringResource(R.string.details_description))
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium)
|
||||
horizontal = dimensionResource(R.dimen.spacing_medium)
|
||||
),
|
||||
text = AnnotatedString.fromHtml(
|
||||
htmlString = app.description
|
||||
@@ -126,7 +124,7 @@ private fun ScreenContent(
|
||||
if (dependencies != null) {
|
||||
AppDependencies(
|
||||
dependencies = dependencies,
|
||||
onNavigateToAppDetails = onNavigateToAppDetails
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -137,7 +135,6 @@ private fun ScreenContent(
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
@@ -148,11 +145,8 @@ private fun ScreenContent(
|
||||
* Composable to show dependencies of an app
|
||||
*/
|
||||
@Composable
|
||||
private fun AppDependencies(
|
||||
dependencies: List<App>,
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit
|
||||
) {
|
||||
Header(title = stringResource(R.string.details_dependencies))
|
||||
private fun AppDependencies(dependencies: List<App>, onNavigateTo: (Destination) -> Unit) {
|
||||
SectionHeader(title = stringResource(R.string.details_dependencies))
|
||||
if (dependencies.isEmpty()) {
|
||||
Info(
|
||||
title = AnnotatedString(text = stringResource(R.string.details_no_dependencies))
|
||||
@@ -162,7 +156,7 @@ private fun AppDependencies(
|
||||
items(items = dependencies, key = { item -> item.id }) { app ->
|
||||
AppListItem(
|
||||
app = app,
|
||||
onClick = { onNavigateToAppDetails(app.packageName) }
|
||||
onClick = { onNavigateTo(Destination.AppDetails(app.packageName)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -174,7 +168,7 @@ private fun AppDependencies(
|
||||
*/
|
||||
@Composable
|
||||
private fun AppInfoMore(app: App) {
|
||||
Header(title = stringResource(R.string.details_more_info))
|
||||
SectionHeader(title = stringResource(R.string.details_more_info))
|
||||
Info(
|
||||
title = AnnotatedString(
|
||||
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.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.aurora.extensions.adaptiveNavigationIcon
|
||||
@@ -47,7 +46,6 @@ import com.aurora.store.viewmodel.details.PermissionViewModel
|
||||
@Composable
|
||||
fun PermissionScreen(
|
||||
packageName: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||
permissionViewModel: PermissionViewModel = hiltViewModel(
|
||||
key = "$packageName/permission",
|
||||
@@ -67,7 +65,6 @@ fun PermissionScreen(
|
||||
|
||||
ScreenContent(
|
||||
topAppBarTitle = topAppBarTitle,
|
||||
onNavigateUp = onNavigateUp,
|
||||
permissionsInfo = permissionsInfo
|
||||
)
|
||||
}
|
||||
@@ -76,7 +73,6 @@ fun PermissionScreen(
|
||||
private fun ScreenContent(
|
||||
topAppBarTitle: String? = null,
|
||||
permissionsInfo: Map<String, PermissionInfo> = emptyMap(),
|
||||
onNavigateUp: () -> Unit = {},
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
val packageManager = LocalContext.current.packageManager
|
||||
@@ -85,8 +81,7 @@ private fun ScreenContent(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = topAppBarTitle,
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -99,9 +94,11 @@ private fun ScreenContent(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
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 ->
|
||||
val permissionInfo = permissionsInfo.getValue(permission)
|
||||
@@ -127,7 +124,6 @@ private fun ScreenContent(
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
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.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.LoadState
|
||||
@@ -50,7 +49,7 @@ import com.aurora.extensions.isWindowCompact
|
||||
import com.aurora.gplayapi.data.models.Review
|
||||
import com.aurora.store.R
|
||||
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.TopAppBar
|
||||
import com.aurora.store.compose.composable.details.ReviewListItem
|
||||
@@ -64,7 +63,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@Composable
|
||||
fun ReviewScreen(
|
||||
packageName: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||
reviewViewModel: ReviewViewModel = hiltViewModel(
|
||||
key = "$packageName/review",
|
||||
@@ -85,7 +83,6 @@ fun ReviewScreen(
|
||||
ScreenContent(
|
||||
topAppBarTitle = topAppBarTitle,
|
||||
reviews = reviews,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onFilter = { filter -> reviewViewModel.fetchReviews(filter) }
|
||||
)
|
||||
}
|
||||
@@ -93,7 +90,6 @@ fun ReviewScreen(
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
topAppBarTitle: String? = null,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
reviews: LazyPagingItems<Review> = emptyPagingItems(),
|
||||
onFilter: (filter: Review.Filter) -> Unit = {},
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
@@ -103,8 +99,7 @@ private fun ScreenContent(
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = topAppBarTitle,
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
FilterHeader { filter -> onFilter(filter) }
|
||||
}
|
||||
@@ -119,7 +114,7 @@ private fun ScreenContent(
|
||||
is LoadState.Loading -> ContainedLoadingIndicator()
|
||||
|
||||
is LoadState.Error -> {
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_disclaimer),
|
||||
message = stringResource(R.string.error)
|
||||
@@ -142,7 +137,6 @@ private fun ScreenContent(
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
@@ -173,8 +167,8 @@ private fun FilterHeader(onClick: (filter: Review.Filter) -> Unit) {
|
||||
|
||||
LazyRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.margin_normal)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal))
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
items(items = filters.keys.toList(), key = { item -> item }) { filter ->
|
||||
val selected = activeFilter == filter
|
||||
|
||||
@@ -17,7 +17,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
@@ -36,11 +36,11 @@ import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
||||
fun ScreenshotScreen(
|
||||
packageName: String,
|
||||
index: Int,
|
||||
onNavigateUp: () -> Unit,
|
||||
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
val app by viewModel.app.collectAsStateWithLifecycle()
|
||||
val screenshots = app?.screenshots ?: emptyList()
|
||||
|
||||
val topAppBarTitle = when {
|
||||
windowAdaptiveInfo.isWindowCompact -> app!!.displayName
|
||||
@@ -49,8 +49,7 @@ fun ScreenshotScreen(
|
||||
|
||||
ScreenContent(
|
||||
topAppBarTitle = topAppBarTitle,
|
||||
onNavigateUp = onNavigateUp,
|
||||
screenshots = app!!.screenshots,
|
||||
screenshots = screenshots.distinctBy { it.url },
|
||||
index = index
|
||||
)
|
||||
}
|
||||
@@ -58,12 +57,11 @@ fun ScreenshotScreen(
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
topAppBarTitle: String? = null,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
screenshots: List<Artwork> = emptyList(),
|
||||
index: Int = 0,
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
val displayMetrics = LocalContext.current.resources.displayMetrics
|
||||
val displayMetrics = LocalResources.current.displayMetrics
|
||||
val pagerState = rememberPagerState(initialPage = index) { screenshots.size }
|
||||
|
||||
LaunchedEffect(key1 = index) {
|
||||
@@ -74,8 +72,7 @@ private fun ScreenContent(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = topAppBarTitle,
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -83,7 +80,8 @@ private fun ScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
state = pagerState
|
||||
state = pagerState,
|
||||
key = { screenshots[it].url }
|
||||
) { page ->
|
||||
val artwork = screenshots[page]
|
||||
ScreenshotListItem(
|
||||
|
||||
@@ -53,8 +53,8 @@ fun Actions(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium))),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
|
||||
.padding(PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium))),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
val buttonWidthModifier = when {
|
||||
windowAdaptiveInfo.isWindowCompact -> Modifier.weight(1F)
|
||||
@@ -92,7 +92,7 @@ fun Actions(
|
||||
@Composable
|
||||
private fun ActionsPreview() {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
Actions(
|
||||
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 com.aurora.gplayapi.data.models.App
|
||||
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.ThemePreviewProvider
|
||||
|
||||
@@ -38,11 +38,11 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
*/
|
||||
@Composable
|
||||
fun Changelog(changelog: String) {
|
||||
Header(title = stringResource(R.string.details_changelog))
|
||||
SectionHeader(title = stringResource(R.string.details_changelog))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dimensionResource(R.dimen.padding_medium))
|
||||
.padding(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -50,8 +50,8 @@ fun Changelog(changelog: String) {
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
||||
.background(color = MaterialTheme.colorScheme.secondaryContainer)
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
horizontal = dimensionResource(R.dimen.spacing_medium),
|
||||
vertical = dimensionResource(R.dimen.spacing_small)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
@@ -71,7 +71,7 @@ fun Changelog(changelog: String) {
|
||||
@Composable
|
||||
private fun ChangelogPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
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.PreviewWrapper
|
||||
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.SectionHeader
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.compose.theme.successColor
|
||||
import com.aurora.store.compose.theme.warningColor
|
||||
@@ -30,7 +30,7 @@ import com.aurora.store.data.model.Scores
|
||||
*/
|
||||
@Composable
|
||||
fun Compatibility(needsGms: Boolean, plexusScores: Scores? = null) {
|
||||
Header(
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.details_compatibility_title),
|
||||
subtitle = stringResource(R.string.plexus_powered)
|
||||
)
|
||||
@@ -82,7 +82,7 @@ fun Compatibility(needsGms: Boolean, plexusScores: Scores? = null) {
|
||||
@Composable
|
||||
private fun CompatibilityPreview() {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
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.Report
|
||||
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.SectionHeader
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
|
||||
@@ -37,7 +37,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
fun DataSafety(report: Report, privacyPolicyUrl: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Header(
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.details_data_safety_title),
|
||||
subtitle = stringResource(R.string.details_data_safety_subtitle),
|
||||
onClick = { context.browse(privacyPolicyUrl) }
|
||||
@@ -86,7 +86,7 @@ fun DataSafety(report: Report, privacyPolicyUrl: String) {
|
||||
@Composable
|
||||
private fun DataSafetyPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
DataSafety(
|
||||
privacyPolicyUrl = app.privacyPolicyUrl,
|
||||
|
||||
@@ -109,7 +109,7 @@ fun Details(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dimensionResource(R.dimen.margin_medium))
|
||||
.padding(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
AnimatedAppIcon(
|
||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
|
||||
@@ -117,7 +117,7 @@ fun Details(
|
||||
inProgress = state.inProgress(),
|
||||
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 = app.displayName,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
@@ -172,7 +172,7 @@ fun Details(
|
||||
@Composable
|
||||
private fun DetailsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
Details(app = app)
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import com.aurora.extensions.copyToClipBoard
|
||||
import com.aurora.extensions.mailTo
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
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.SectionHeader
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
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) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Header(title = stringResource(R.string.details_dev_details))
|
||||
SectionHeader(title = stringResource(R.string.details_dev_details))
|
||||
Column {
|
||||
if (website.isNotBlank()) {
|
||||
Info(
|
||||
@@ -74,7 +74,7 @@ fun DeveloperDetails(address: String, website: String, email: String) {
|
||||
@Composable
|
||||
private fun DeveloperDetailsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
DeveloperDetails(
|
||||
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.PreviewWrapper
|
||||
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.SectionHeader
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.compose.theme.successColor
|
||||
import com.aurora.store.compose.theme.warningColor
|
||||
@@ -30,7 +30,7 @@ import com.aurora.store.data.model.Report
|
||||
*/
|
||||
@Composable
|
||||
fun Privacy(report: Report?, onNavigateToDetailsExodus: (() -> Unit)? = null) {
|
||||
Header(
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.details_privacy),
|
||||
subtitle = stringResource(R.string.exodus_powered),
|
||||
onClick = onNavigateToDetailsExodus
|
||||
@@ -64,7 +64,7 @@ fun Privacy(report: Report?, onNavigateToDetailsExodus: (() -> Unit)? = null) {
|
||||
@Composable
|
||||
private fun PrivacyPreview() {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
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.Review
|
||||
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.ReviewListItem
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
@@ -82,7 +82,7 @@ fun RatingAndReviews(
|
||||
}
|
||||
}
|
||||
|
||||
Header(
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.details_ratings),
|
||||
onClick = onNavigateToDetailsReview
|
||||
)
|
||||
@@ -90,12 +90,12 @@ fun RatingAndReviews(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
||||
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_large))
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_large))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)),
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
@@ -114,7 +114,7 @@ fun RatingAndReviews(
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||
) {
|
||||
stars.reversed().fastForEach { star ->
|
||||
RatingListItem(
|
||||
@@ -129,8 +129,8 @@ fun RatingAndReviews(
|
||||
val pagerState = rememberPagerState { featuredReviews.size }
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
pageSpacing = dimensionResource(R.dimen.margin_medium)
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
pageSpacing = dimensionResource(R.dimen.spacing_medium)
|
||||
) { page ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -158,7 +158,7 @@ private fun RatingAndReviewsPreview(@PreviewParameter(AppPreviewProvider::class)
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
RatingAndReviews(rating = app.rating, featuredReviews = reviews)
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
@Composable
|
||||
fun Screenshots(screenshots: List<Artwork>, onNavigateToScreenshot: (index: Int) -> Unit = {}) {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||
) {
|
||||
items(items = screenshots, key = { artwork -> artwork.url }) { artwork ->
|
||||
ScreenshotListItem(
|
||||
@@ -57,7 +57,7 @@ fun Screenshots(screenshots: List<Artwork>, onNavigateToScreenshot: (index: Int)
|
||||
@Composable
|
||||
private fun ScreenshotsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
Screenshots(screenshots = app.screenshots)
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ fun Tags(app: App) {
|
||||
).filterKeys { it != null }
|
||||
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
items(items = tags.keys.toList()) { label ->
|
||||
TagListItem(label = label!!, painter = painterResource(tags.getValue(label)))
|
||||
@@ -77,7 +77,7 @@ fun Tags(app: App) {
|
||||
@Composable
|
||||
private fun TagsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
Tags(app = app)
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
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.SectionHeader
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
|
||||
@@ -40,13 +40,13 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
*/
|
||||
@Composable
|
||||
fun Testing(isSubscribed: Boolean, onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}) {
|
||||
Header(title = stringResource(R.string.details_beta))
|
||||
SectionHeader(title = stringResource(R.string.details_beta))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
||||
.padding(dimensionResource(R.dimen.spacing_medium)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
|
||||
) {
|
||||
Info(
|
||||
modifier = Modifier.weight(1F),
|
||||
@@ -79,7 +79,7 @@ fun Testing(isSubscribed: Boolean, onTestingSubscriptionChange: (subscribe: Bool
|
||||
@Composable
|
||||
private fun TestingPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
Testing(isSubscribed = app.testingProgram!!.isSubscribed)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package com.aurora.store.compose.ui.dev
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.store.R
|
||||
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.app.LargeAppListItem
|
||||
import com.aurora.store.compose.navigation.Destination
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.viewmodel.details.DevProfileViewModel
|
||||
@@ -48,8 +50,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@Composable
|
||||
fun DevProfileScreen(
|
||||
developerId: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||
onNavigateTo: (Destination) -> Unit,
|
||||
viewModel: DevProfileViewModel = hiltViewModel()
|
||||
) {
|
||||
// TODO: Implement when migrating logic for current DevProfileFragment
|
||||
@@ -62,8 +63,7 @@ fun DevProfileScreen(
|
||||
@Composable
|
||||
fun DevProfileScreen(
|
||||
publisherId: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||
onNavigateTo: (Destination) -> Unit,
|
||||
viewModel: SearchViewModel = hiltViewModel()
|
||||
) {
|
||||
val apps = viewModel.apps.collectAsLazyPagingItems()
|
||||
@@ -73,8 +73,7 @@ fun DevProfileScreen(
|
||||
ScreenContent(
|
||||
apps = apps,
|
||||
topAppBarTitle = publisherId,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onNavigateToAppDetails = onNavigateToAppDetails
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,16 +81,14 @@ fun DevProfileScreen(
|
||||
private fun ScreenContent(
|
||||
apps: LazyPagingItems<App>,
|
||||
topAppBarTitle: String,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
||||
onNavigateTo: (Destination) -> Unit = {},
|
||||
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = topAppBarTitle,
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
|
||||
onNavigateUp = onNavigateUp
|
||||
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -99,7 +96,7 @@ private fun ScreenContent(
|
||||
is LoadState.Loading -> ContainedLoadingIndicator()
|
||||
|
||||
is LoadState.Error -> {
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_disclaimer),
|
||||
message = stringResource(R.string.error)
|
||||
@@ -111,13 +108,16 @@ private fun ScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.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 ->
|
||||
apps[index]?.let { app ->
|
||||
LargeAppListItem(
|
||||
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.store.R
|
||||
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.preview.ThemePreviewProvider
|
||||
import com.aurora.store.viewmodel.dispenser.DispenserViewModel
|
||||
|
||||
@Composable
|
||||
fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hiltViewModel()) {
|
||||
fun DispenserScreen(viewModel: DispenserViewModel = hiltViewModel()) {
|
||||
val dispensers by viewModel.dispensers.collectAsStateWithLifecycle()
|
||||
|
||||
var shouldShowInputDialog by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -63,7 +63,6 @@ fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hi
|
||||
|
||||
ScreenContent(
|
||||
dispensers = dispensers,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onAddDispenser = { shouldShowInputDialog = true },
|
||||
onRemoveDispenser = { url -> shouldRemoveDispenser = url }
|
||||
)
|
||||
@@ -71,7 +70,6 @@ fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hi
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
onNavigateUp: () -> Unit = {},
|
||||
dispensers: Set<String> = emptySet(),
|
||||
onAddDispenser: () -> Unit = {},
|
||||
onRemoveDispenser: (url: String) -> Unit = {}
|
||||
@@ -81,8 +79,7 @@ private fun ScreenContent(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(R.string.pref_dispenser_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
title = stringResource(R.string.pref_dispenser_title)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
@@ -97,11 +94,11 @@ private fun ScreenContent(
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (dispensers.isEmpty()) {
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_server),
|
||||
message = stringResource(R.string.no_dispensers_available),
|
||||
actionMessage = stringResource(R.string.add_dispenser_title),
|
||||
actionLabel = stringResource(R.string.add_dispenser_title),
|
||||
onAction = onAddDispenser
|
||||
)
|
||||
} else {
|
||||
@@ -109,7 +106,7 @@ private fun ScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
||||
.padding(vertical = dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
items(items = dispensers.toList(), key = { url -> url }) { url ->
|
||||
DispenserListItem(
|
||||
|
||||
@@ -53,7 +53,9 @@ fun InputDispenserDialog(onAdd: (url: String) -> Unit = {}, onDismiss: () -> Uni
|
||||
title = { Text(text = stringResource(R.string.add_dispenser_title)) },
|
||||
text = {
|
||||
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))
|
||||
OutlinedTextField(
|
||||
|
||||
@@ -29,7 +29,6 @@ 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 androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
@@ -42,13 +41,15 @@ import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||
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.TopAppBar
|
||||
import com.aurora.store.compose.navigation.Destination
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.compose.ui.downloads.menu.DownloadsMenu
|
||||
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.room.download.Download
|
||||
import com.aurora.store.viewmodel.downloads.DownloadsViewModel
|
||||
@@ -57,9 +58,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
fun DownloadsScreen(
|
||||
onNavigateUp: () -> Unit,
|
||||
viewModel: DownloadsViewModel = hiltViewModel(),
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit
|
||||
onNavigateTo: (Destination) -> Unit,
|
||||
viewModel: DownloadsViewModel = hiltViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val downloads = viewModel.downloads.collectAsLazyPagingItems()
|
||||
@@ -79,9 +79,8 @@ fun DownloadsScreen(
|
||||
)
|
||||
|
||||
ScreenContent(
|
||||
onNavigateUp = onNavigateUp,
|
||||
downloads = downloads,
|
||||
onNavigateToAppDetails = onNavigateToAppDetails,
|
||||
onNavigateTo = onNavigateTo,
|
||||
onCancelAll = { viewModel.cancelAll() },
|
||||
onForceClearAll = { viewModel.clearAll() },
|
||||
onClearFinished = { viewModel.clearFinished() },
|
||||
@@ -100,8 +99,7 @@ fun DownloadsScreen(
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
downloads: LazyPagingItems<Download> = emptyPagingItems(),
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
||||
onNavigateTo: (Destination) -> Unit = {},
|
||||
onCancel: (packageName: String) -> Unit = {},
|
||||
onClear: (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.
|
||||
*/
|
||||
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
|
||||
fun SetupMenu() {
|
||||
@@ -133,7 +144,6 @@ private fun ScreenContent(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(R.string.title_download_manager),
|
||||
onNavigateUp = onNavigateUp,
|
||||
actions = { if (downloads.itemCount != 0) SetupMenu() }
|
||||
)
|
||||
}
|
||||
@@ -142,7 +152,7 @@ private fun ScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
||||
.padding(vertical = dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
when {
|
||||
downloads.loadState.refresh is LoadState.Loading && initialLoad -> {
|
||||
@@ -153,7 +163,7 @@ private fun ScreenContent(
|
||||
initialLoad = false
|
||||
|
||||
if (downloads.itemCount == 0) {
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_download_manager),
|
||||
message = stringResource(R.string.download_none)
|
||||
@@ -172,20 +182,13 @@ private fun ScreenContent(
|
||||
DownloadListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
download = download,
|
||||
onClick = {
|
||||
onNavigateToAppDetails(download.packageName)
|
||||
},
|
||||
onClear = { onClear(download) },
|
||||
onCancel = { onCancel(download.packageName) },
|
||||
onExport = { onExport(download) },
|
||||
onInstall = { onInstall(download) }
|
||||
onClick = { actionsTarget = download }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
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.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
@@ -41,10 +40,11 @@ import com.aurora.extensions.emptyPagingItems
|
||||
import com.aurora.extensions.toast
|
||||
import com.aurora.store.R
|
||||
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.Placeholder
|
||||
import com.aurora.store.compose.composable.ScrollHint
|
||||
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.ThemePreviewProvider
|
||||
import com.aurora.store.compose.ui.favourite.menu.FavouriteMenu
|
||||
@@ -57,8 +57,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
fun FavouriteScreen(
|
||||
onNavigateUp: () -> Unit,
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||
onNavigateTo: (Destination) -> Unit,
|
||||
viewModel: FavouriteViewModel = hiltViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -90,8 +89,7 @@ fun FavouriteScreen(
|
||||
|
||||
ScreenContent(
|
||||
favourites = favourites,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onNavigateToAppDetails = onNavigateToAppDetails,
|
||||
onNavigateTo = onNavigateTo,
|
||||
onRemoveFavourite = { packageName -> viewModel.removeFavourite(packageName) },
|
||||
onImportFavourites = {
|
||||
documentOpenLauncher.launch(arrayOf(JSON_MIME_TYPE))
|
||||
@@ -107,8 +105,7 @@ fun FavouriteScreen(
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
favourites: LazyPagingItems<Favourite> = emptyPagingItems(),
|
||||
onNavigateUp: () -> Unit = {},
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
||||
onNavigateTo: (Destination) -> Unit = {},
|
||||
onRemoveFavourite: (packageName: String) -> Unit = {},
|
||||
onImportFavourites: () -> Unit = {},
|
||||
onExportFavourites: () -> Unit = {}
|
||||
@@ -135,7 +132,6 @@ private fun ScreenContent(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(R.string.title_favourites_manager),
|
||||
onNavigateUp = onNavigateUp,
|
||||
actions = { SetupMenu() }
|
||||
)
|
||||
}
|
||||
@@ -144,7 +140,7 @@ private fun ScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
||||
.padding(vertical = dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
when {
|
||||
favourites.loadState.refresh is LoadState.Loading && initialLoad -> {
|
||||
@@ -155,7 +151,7 @@ private fun ScreenContent(
|
||||
initialLoad = false
|
||||
|
||||
if (favourites.itemCount == 0) {
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_favorite_unchecked),
|
||||
message = stringResource(R.string.details_no_favourites)
|
||||
@@ -175,7 +171,9 @@ private fun ScreenContent(
|
||||
modifier = Modifier.animateItem(),
|
||||
favourite = favourite,
|
||||
onClick = {
|
||||
onNavigateToAppDetails(favourite.packageName)
|
||||
onNavigateTo(
|
||||
Destination.AppDetails(favourite.packageName)
|
||||
)
|
||||
},
|
||||
onClear = { onRemoveFavourite(favourite.packageName) }
|
||||
)
|
||||
@@ -184,7 +182,6 @@ private fun ScreenContent(
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,27 +6,32 @@
|
||||
|
||||
package com.aurora.store.compose.ui.installed
|
||||
|
||||
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.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.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.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
@@ -36,36 +41,48 @@ import com.aurora.extensions.emptyPagingItems
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.store.R
|
||||
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.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.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 kotlin.random.Random
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
fun InstalledScreen(
|
||||
onNavigateUp: () -> Unit,
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||
onNavigateTo: (Destination) -> Unit,
|
||||
viewModel: InstalledViewModel = hiltViewModel()
|
||||
) {
|
||||
val apps = viewModel.apps.collectAsLazyPagingItems()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val installers by viewModel.installers.collectAsStateWithLifecycle()
|
||||
val metadata by viewModel.metadata.collectAsStateWithLifecycle()
|
||||
|
||||
ScreenContent(
|
||||
apps = apps,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onNavigateToAppDetails = onNavigateToAppDetails
|
||||
state = state,
|
||||
installers = installers,
|
||||
metadata = metadata,
|
||||
onStateChange = viewModel::updateState,
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
onNavigateUp: () -> Unit = {},
|
||||
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
|
||||
@@ -74,12 +91,20 @@ private fun ScreenContent(
|
||||
* Save the initial loading state to make sure we don't replay the loading animation again.
|
||||
*/
|
||||
var initialLoad by rememberSaveable { mutableStateOf(true) }
|
||||
var sheetVisible by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
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 ->
|
||||
@@ -97,7 +122,7 @@ private fun ScreenContent(
|
||||
initialLoad = false
|
||||
|
||||
if (apps.itemCount == 0) {
|
||||
Error(
|
||||
Placeholder(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
painter = painterResource(R.drawable.ic_apps_outage),
|
||||
message = stringResource(R.string.no_apps_available)
|
||||
@@ -106,23 +131,34 @@ private fun ScreenContent(
|
||||
val listState = rememberLazyListState()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.spacing_medium)
|
||||
)
|
||||
) {
|
||||
items(
|
||||
count = apps.itemCount,
|
||||
key = apps.itemKey { it.packageName }
|
||||
) { index ->
|
||||
apps[index]?.let { app ->
|
||||
LargeAppListItem(
|
||||
val meta = metadata[app.packageName]
|
||||
InstalledAppListItem(
|
||||
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(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
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)
|
||||
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.aurora.extensions.toast
|
||||
import com.aurora.store.R
|
||||
@@ -62,12 +61,12 @@ private fun ScreenContent(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
state = listState
|
||||
) {
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.onboarding_title_gsf),
|
||||
@@ -105,7 +104,6 @@ private fun ScreenContent(
|
||||
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,16 +99,16 @@ private fun ScreenContent(
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
vertical = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.spacing_medium),
|
||||
horizontal = if (windowAdaptiveInfo.isWindowCompact) {
|
||||
dimensionResource(R.dimen.padding_medium)
|
||||
dimensionResource(R.dimen.spacing_medium)
|
||||
} else {
|
||||
dimensionResource(R.dimen.padding_xlarge)
|
||||
dimensionResource(R.dimen.spacing_xlarge)
|
||||
}
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.padding_medium),
|
||||
dimensionResource(R.dimen.spacing_medium),
|
||||
Alignment.End
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -45,12 +45,12 @@ private fun PageContent(
|
||||
onPermissionCallback: (type: PermissionType) -> Unit = {}
|
||||
) {
|
||||
PermissionList(
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
permissions = permissions,
|
||||
onPermissionCallback = onPermissionCallback,
|
||||
header = {
|
||||
Column(
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.onboarding_title_permissions),
|
||||
|
||||
@@ -96,12 +96,12 @@ private fun PageContent(onAboutAurora: () -> Unit = {}) {
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.spacing_medium)),
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_xsmall))
|
||||
) {
|
||||
stickyHeader {
|
||||
Column(
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
Text(
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hiltViewModel()) {
|
||||
fun InstallerScreen(viewModel: InstallerViewModel = hiltViewModel()) {
|
||||
val currentInstallerId by viewModel.currentInstaller.collectAsStateWithLifecycle()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
@@ -46,7 +46,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi
|
||||
}
|
||||
|
||||
ScreenContent(
|
||||
onNavigateUp = onNavigateUp,
|
||||
snackBarHostState = snackBarHostState,
|
||||
currentInstaller = Installer.entries[currentInstallerId],
|
||||
availableInstallers = AppInstaller.getAvailableInstallersInfo(LocalContext.current),
|
||||
@@ -56,7 +55,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
onNavigateUp: () -> Unit = {},
|
||||
snackBarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
currentInstaller: Installer = Installer.SESSION,
|
||||
availableInstallers: List<InstallerInfo> = emptyList(),
|
||||
@@ -70,8 +68,7 @@ private fun ScreenContent(
|
||||
},
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(R.string.pref_install_mode_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
title = stringResource(R.string.pref_install_mode_title)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -79,7 +76,7 @@ private fun ScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
||||
.padding(vertical = dimensionResource(R.dimen.spacing_medium))
|
||||
) {
|
||||
items(items = availableInstallers, key = { i -> i.id }) { installerInfo ->
|
||||
InstallerListItem(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user