Feat(Tool): Add tool store
Add tool store, support install tool online.
This commit is contained in:
@@ -11,6 +11,8 @@ plugins {
|
|||||||
alias(libs.plugins.protobuf)
|
alias(libs.plugins.protobuf)
|
||||||
alias(libs.plugins.kotlinxSerialization)
|
alias(libs.plugins.kotlinxSerialization)
|
||||||
alias(libs.plugins.secrets)
|
alias(libs.plugins.secrets)
|
||||||
|
alias(libs.plugins.room)
|
||||||
|
alias(libs.plugins.parcelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -120,6 +122,14 @@ secrets {
|
|||||||
defaultPropertiesFileName = "secrets.defaults.properties"
|
defaultPropertiesFileName = "secrets.defaults.properties"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.generateKotlin", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
room {
|
||||||
|
schemaDirectory("$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
@@ -171,4 +181,7 @@ dependencies {
|
|||||||
implementation(libs.paging.compose)
|
implementation(libs.paging.compose)
|
||||||
implementation(libs.androidsvg.aar)
|
implementation(libs.androidsvg.aar)
|
||||||
implementation(libs.compose.webview)
|
implementation(libs.compose.webview)
|
||||||
|
ksp(libs.room.compiler)
|
||||||
|
implementation(libs.room.runtime)
|
||||||
|
implementation(libs.room.ktx)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "adfae7fd1829b1afdfd27eb282388074",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "tools",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `toolId` TEXT NOT NULL, `icon` TEXT NOT NULL, `platform` TEXT NOT NULL, `description` TEXT, `base` TEXT, `authorUsername` TEXT NOT NULL, `authorNickname` TEXT NOT NULL, `authorAvatar` TEXT NOT NULL, `ver` TEXT NOT NULL, `keywords` TEXT NOT NULL, `categories` TEXT NOT NULL, `source` TEXT, `dist` TEXT, `entryPoint` TEXT NOT NULL, `createTime` TEXT NOT NULL, `updateTime` TEXT NOT NULL, `isStar` INTEGER NOT NULL DEFAULT false, `upgrade` TEXT DEFAULT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "toolId",
|
||||||
|
"columnName": "toolId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "platform",
|
||||||
|
"columnName": "platform",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "base",
|
||||||
|
"columnName": "base",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorUsername",
|
||||||
|
"columnName": "authorUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorNickname",
|
||||||
|
"columnName": "authorNickname",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorAvatar",
|
||||||
|
"columnName": "authorAvatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ver",
|
||||||
|
"columnName": "ver",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "keywords",
|
||||||
|
"columnName": "keywords",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "categories",
|
||||||
|
"columnName": "categories",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "dist",
|
||||||
|
"columnName": "dist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "entryPoint",
|
||||||
|
"columnName": "entryPoint",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createTime",
|
||||||
|
"columnName": "createTime",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updateTime",
|
||||||
|
"columnName": "updateTime",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isStar",
|
||||||
|
"columnName": "isStar",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "upgrade",
|
||||||
|
"columnName": "upgrade",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "NULL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"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, 'adfae7fd1829b1afdfd27eb282388074')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.data.tool
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.dao.ToolDao
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [ToolEntity::class],
|
||||||
|
version = 1,
|
||||||
|
autoMigrations = [],
|
||||||
|
exportSchema = true
|
||||||
|
)
|
||||||
|
abstract class ToolDatabase : RoomDatabase() {
|
||||||
|
abstract fun toolDao(): ToolDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: ToolDatabase? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context): ToolDatabase =
|
||||||
|
INSTANCE ?: synchronized(this) {
|
||||||
|
Room.databaseBuilder(context, ToolDatabase::class.java, "tools.db")
|
||||||
|
.build()
|
||||||
|
.also { INSTANCE = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.data.tool.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface ToolDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertTool(tool: ToolEntity)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun updateTool(tool: ToolEntity)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteTool(tool: ToolEntity)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tools WHERE id = :id")
|
||||||
|
fun selectToolById(id: Long): Flow<ToolEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tools ORDER BY updateTime DESC")
|
||||||
|
fun selectAllTools(): Flow<List<ToolEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tools WHERE authorUsername = :username and toolId = :toolId LIMIT 1")
|
||||||
|
fun selectToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?>
|
||||||
|
}
|
||||||
@@ -10,10 +10,12 @@ import top.fatweb.oxygen.toolbox.monitor.TimeZoneBroadcastMonitor
|
|||||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||||
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
|
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
|
||||||
import top.fatweb.oxygen.toolbox.repository.lib.impl.LocalDepRepository
|
import top.fatweb.oxygen.toolbox.repository.lib.impl.LocalDepRepository
|
||||||
import top.fatweb.oxygen.toolbox.repository.tool.impl.NetworkToolRepository
|
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
|
||||||
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
||||||
import top.fatweb.oxygen.toolbox.repository.userdata.impl.LocalUserDataRepository
|
import top.fatweb.oxygen.toolbox.repository.tool.impl.NetworkStoreRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.impl.OfflineToolRepository
|
||||||
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
|
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.userdata.impl.LocalUserDataRepository
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@@ -28,8 +30,11 @@ abstract class DataModule {
|
|||||||
internal abstract fun bindsUserDataRepository(userDataRepository: LocalUserDataRepository): UserDataRepository
|
internal abstract fun bindsUserDataRepository(userDataRepository: LocalUserDataRepository): UserDataRepository
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
internal abstract fun bindsToolRepository(toolRepository: NetworkToolRepository): ToolRepository
|
internal abstract fun bindsDepRepository(depRepository: LocalDepRepository): DepRepository
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
internal abstract fun bindsDepRepository(depRepository: LocalDepRepository): DepRepository
|
internal abstract fun bindsStoreRepository(storeRepository: NetworkStoreRepository): StoreRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
internal abstract fun bindsToolRepository(toolRepository: OfflineToolRepository): ToolRepository
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.ToolDatabase
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.dao.ToolDao
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object DatabaseModule {
|
||||||
|
@Provides
|
||||||
|
fun provideToolDao(@ApplicationContext context: Context): ToolDao =
|
||||||
|
ToolDatabase.getInstance(context).toolDao()
|
||||||
|
}
|
||||||
@@ -5,19 +5,25 @@ import android.graphics.drawable.PictureDrawable
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AccessTime
|
import androidx.compose.material.icons.filled.AccessTime
|
||||||
import androidx.compose.material.icons.filled.Build
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Code
|
import androidx.compose.material.icons.filled.Code
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
import androidx.compose.material.icons.filled.Inbox
|
import androidx.compose.material.icons.filled.Inbox
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Reorder
|
import androidx.compose.material.icons.filled.Reorder
|
||||||
import androidx.compose.material.icons.outlined.Home
|
import androidx.compose.material.icons.outlined.Home
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.StarBorder
|
import androidx.compose.material.icons.outlined.StarBorder
|
||||||
|
import androidx.compose.material.icons.outlined.Store
|
||||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||||
|
import androidx.compose.material.icons.rounded.CheckCircle
|
||||||
import androidx.compose.material.icons.rounded.Home
|
import androidx.compose.material.icons.rounded.Home
|
||||||
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
||||||
import androidx.compose.material.icons.rounded.Search
|
import androidx.compose.material.icons.rounded.Search
|
||||||
import androidx.compose.material.icons.rounded.Star
|
import androidx.compose.material.icons.rounded.Star
|
||||||
|
import androidx.compose.material.icons.rounded.Store
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
@@ -33,6 +39,8 @@ object OxygenIcons {
|
|||||||
val Box = Icons.Default.Inbox
|
val Box = Icons.Default.Inbox
|
||||||
val Close = Icons.Default.Close
|
val Close = Icons.Default.Close
|
||||||
val Code = Icons.Default.Code
|
val Code = Icons.Default.Code
|
||||||
|
val Download = Icons.Default.Download
|
||||||
|
val Error = Icons.Default.Cancel
|
||||||
val Home = Icons.Rounded.Home
|
val Home = Icons.Rounded.Home
|
||||||
val HomeBorder = Icons.Outlined.Home
|
val HomeBorder = Icons.Outlined.Home
|
||||||
val Info = Icons.Outlined.Info
|
val Info = Icons.Outlined.Info
|
||||||
@@ -41,6 +49,9 @@ object OxygenIcons {
|
|||||||
val Search = Icons.Rounded.Search
|
val Search = Icons.Rounded.Search
|
||||||
val Star = Icons.Rounded.Star
|
val Star = Icons.Rounded.Star
|
||||||
val StarBorder = Icons.Outlined.StarBorder
|
val StarBorder = Icons.Outlined.StarBorder
|
||||||
|
val Store = Icons.Rounded.Store
|
||||||
|
val StoreBorder = Icons.Outlined.Store
|
||||||
|
val Success = Icons.Rounded.CheckCircle
|
||||||
val Time = Icons.Default.AccessTime
|
val Time = Icons.Default.AccessTime
|
||||||
val Tool = Icons.Default.Build
|
val Tool = Icons.Default.Build
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity.Platform
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromPlatform(platform: Platform): String = platform.name
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toPlatform(name: String): Platform = Platform.valueOf(name)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromStringList(stringList: List<String>): String = json.encodeToString(stringList)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toStringList(stringList: String): List<String> = json.decodeFromString(stringList)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromLocalDateTime(localDateTime: LocalDateTime): String = localDateTime.toString()
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toLocalDateTime(string: String): LocalDateTime = LocalDateTime.parse(string)
|
||||||
|
}
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
package top.fatweb.oxygen.toolbox.model.tool
|
package top.fatweb.oxygen.toolbox.model.tool
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.TypeConverters
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Converters
|
||||||
|
|
||||||
data class Tool(
|
@Entity(tableName = "tools")
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
data class ToolEntity(
|
||||||
|
@PrimaryKey
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -17,7 +25,11 @@ data class Tool(
|
|||||||
|
|
||||||
val base: String? = null,
|
val base: String? = null,
|
||||||
|
|
||||||
val author: Author,
|
val authorUsername: String,
|
||||||
|
|
||||||
|
val authorNickname: String,
|
||||||
|
|
||||||
|
val authorAvatar: String,
|
||||||
|
|
||||||
val ver: String,
|
val ver: String,
|
||||||
|
|
||||||
@@ -33,7 +45,13 @@ data class Tool(
|
|||||||
|
|
||||||
val createTime: LocalDateTime,
|
val createTime: LocalDateTime,
|
||||||
|
|
||||||
val updateTime: LocalDateTime
|
val updateTime: LocalDateTime,
|
||||||
|
|
||||||
|
@ColumnInfo(defaultValue = "false")
|
||||||
|
val isStar: Boolean = false,
|
||||||
|
|
||||||
|
@ColumnInfo(defaultValue = "NULL")
|
||||||
|
val upgrade: String? = null
|
||||||
) {
|
) {
|
||||||
enum class Platform {
|
enum class Platform {
|
||||||
WEB,
|
WEB,
|
||||||
@@ -42,12 +60,4 @@ data class Tool(
|
|||||||
|
|
||||||
ANDROID
|
ANDROID
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Author(
|
|
||||||
val username: String,
|
|
||||||
|
|
||||||
val nickname: String,
|
|
||||||
|
|
||||||
val avatar: String
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -10,5 +10,5 @@ data class ToolGroup(
|
|||||||
|
|
||||||
val title: String,
|
val title: String,
|
||||||
|
|
||||||
val tools: List<Tool> = emptyList()
|
val tools: List<ToolEntity> = emptyList()
|
||||||
)
|
)
|
||||||
@@ -28,9 +28,12 @@ fun OxygenNavHost(
|
|||||||
librariesScreen(
|
librariesScreen(
|
||||||
onBackClick = navController::popBackStack
|
onBackClick = navController::popBackStack
|
||||||
)
|
)
|
||||||
|
toolStoreScreen(
|
||||||
|
onNavigateToToolView = navController::navigateToToolView
|
||||||
|
)
|
||||||
toolsScreen(
|
toolsScreen(
|
||||||
onNavigateToToolView = navController::navigateToToolView,
|
onNavigateToToolView = navController::navigateToToolView,
|
||||||
onShowSnackbar = onShowSnackbar
|
onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.TOOL_STORE) }
|
||||||
)
|
)
|
||||||
toolViewScreen(
|
toolViewScreen(
|
||||||
onBackClick = navController::popBackStack
|
onBackClick = navController::popBackStack
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.tool.ToolStoreRoute
|
||||||
|
|
||||||
|
const val TOOL_STORE_ROUTE = "tool_store_route"
|
||||||
|
|
||||||
|
fun NavController.navigateToToolStore(navOptions: NavOptions? = null) = navigate(TOOL_STORE_ROUTE, navOptions)
|
||||||
|
|
||||||
|
fun NavGraphBuilder.toolStoreScreen(
|
||||||
|
onNavigateToToolView: (username: String, toolId: String) -> Unit
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
route = TOOL_STORE_ROUTE
|
||||||
|
) {
|
||||||
|
ToolStoreRoute(
|
||||||
|
onNavigateToToolView = onNavigateToToolView
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,14 +12,14 @@ fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE
|
|||||||
|
|
||||||
fun NavGraphBuilder.toolsScreen(
|
fun NavGraphBuilder.toolsScreen(
|
||||||
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||||
onShowSnackbar: suspend (String, String?) -> Boolean
|
onNavigateToToolStore: () -> Unit
|
||||||
) {
|
) {
|
||||||
composable(
|
composable(
|
||||||
route = TOOLS_ROUTE
|
route = TOOLS_ROUTE
|
||||||
) {
|
) {
|
||||||
ToolsRoute(
|
ToolsRoute(
|
||||||
onNavigateToToolView = onNavigateToToolView,
|
onNavigateToToolView = onNavigateToToolView,
|
||||||
onShowSnackbar = onShowSnackbar
|
onNavigateToToolStore = onNavigateToToolStore
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,13 @@ enum class TopLevelDestination(
|
|||||||
@StringRes val iconTextId: Int,
|
@StringRes val iconTextId: Int,
|
||||||
@StringRes val titleTextId: Int
|
@StringRes val titleTextId: Int
|
||||||
) {
|
) {
|
||||||
|
TOOL_STORE(
|
||||||
|
selectedIcon = OxygenIcons.Store,
|
||||||
|
unselectedIcon = OxygenIcons.StoreBorder,
|
||||||
|
iconTextId = R.string.feature_store_title,
|
||||||
|
titleTextId = R.string.feature_store_title
|
||||||
|
),
|
||||||
|
|
||||||
TOOLS(
|
TOOLS(
|
||||||
selectedIcon = OxygenIcons.Home,
|
selectedIcon = OxygenIcons.Home,
|
||||||
unselectedIcon = OxygenIcons.HomeBorder,
|
unselectedIcon = OxygenIcons.HomeBorder,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package top.fatweb.oxygen.toolbox.network.model
|
|||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
|
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -39,4 +39,4 @@ data class ToolBaseVo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ToolBaseVo.Platform.asExternalModel() = Tool.Platform.valueOf(this.name)
|
fun ToolBaseVo.Platform.asExternalModel() = ToolEntity.Platform.valueOf(this.name)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package top.fatweb.oxygen.toolbox.network.model
|
|||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
|
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -62,7 +62,7 @@ data class ToolVo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ToolVo.asExternalModel() = Tool(
|
fun ToolVo.asExternalModel() = ToolEntity(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
toolId = toolId,
|
toolId = toolId,
|
||||||
@@ -70,7 +70,9 @@ fun ToolVo.asExternalModel() = Tool(
|
|||||||
platform = platform.asExternalModel(),
|
platform = platform.asExternalModel(),
|
||||||
description = description,
|
description = description,
|
||||||
base = base?.dist?.data,
|
base = base?.dist?.data,
|
||||||
author = author.asExternalModel(),
|
authorUsername = author.username,
|
||||||
|
authorNickname = author.userInfo.nickname,
|
||||||
|
authorAvatar = author.userInfo.avatar,
|
||||||
ver = ver,
|
ver = ver,
|
||||||
keywords = keywords,
|
keywords = keywords,
|
||||||
categories = categories.map { it.name },
|
categories = categories.map { it.name },
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package top.fatweb.oxygen.toolbox.network.model
|
package top.fatweb.oxygen.toolbox.network.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserWithInfoVo(
|
data class UserWithInfoVo(
|
||||||
@@ -20,9 +19,3 @@ data class UserWithInfoVo(
|
|||||||
val avatar: String
|
val avatar: String
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun UserWithInfoVo.asExternalModel() = Tool.Author(
|
|
||||||
username = username,
|
|
||||||
nickname = userInfo.nickname,
|
|
||||||
avatar = userInfo.avatar
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ package top.fatweb.oxygen.toolbox.network.paging
|
|||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
|
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
import top.fatweb.oxygen.toolbox.network.model.ToolVo
|
import top.fatweb.oxygen.toolbox.network.model.ToolVo
|
||||||
import top.fatweb.oxygen.toolbox.network.model.asExternalModel
|
import top.fatweb.oxygen.toolbox.network.model.asExternalModel
|
||||||
|
|
||||||
internal class ToolStorePagingSource(
|
internal class ToolStorePagingSource(
|
||||||
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
|
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
|
||||||
private val searchValue: String
|
private val searchValue: String
|
||||||
) : PagingSource<Int, Tool>() {
|
) : PagingSource<Int, ToolEntity>() {
|
||||||
override fun getRefreshKey(state: PagingState<Int, Tool>): Int? = null
|
override fun getRefreshKey(state: PagingState<Int, ToolEntity>): Int? = null
|
||||||
|
|
||||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Tool> {
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ToolEntity> {
|
||||||
return try {
|
return try {
|
||||||
val currentPage = params.key ?: 1
|
val currentPage = params.key ?: 1
|
||||||
val (_, success, msg, data) = oxygenNetworkDataSource.getStore(searchValue, currentPage)
|
val (_, success, msg, data) = oxygenNetworkDataSource.getStore(searchValue, currentPage)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.repository.tool
|
||||||
|
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
|
||||||
|
interface StoreRepository {
|
||||||
|
val toolViewTemplate: Flow<String>
|
||||||
|
|
||||||
|
suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<ToolEntity>>
|
||||||
|
|
||||||
|
fun detail(
|
||||||
|
username: String,
|
||||||
|
toolId: String,
|
||||||
|
ver: String = "latest",
|
||||||
|
platform: ToolEntity.Platform = ToolEntity.Platform.ANDROID
|
||||||
|
): Flow<Result<ToolEntity>>
|
||||||
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
package top.fatweb.oxygen.toolbox.repository.tool
|
package top.fatweb.oxygen.toolbox.repository.tool
|
||||||
|
|
||||||
import androidx.paging.PagingData
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import top.fatweb.oxygen.toolbox.model.Result
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
|
||||||
|
|
||||||
interface ToolRepository {
|
interface ToolRepository {
|
||||||
val toolViewTemplate: Flow<String>
|
fun getAllToolsStream(): Flow<List<ToolEntity>>
|
||||||
|
|
||||||
suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<Tool>>
|
fun getToolById(id: Long): Flow<ToolEntity?>
|
||||||
|
|
||||||
fun detail(
|
fun getToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?>
|
||||||
username: String,
|
|
||||||
toolId: String,
|
suspend fun saveTool(toolEntity: ToolEntity)
|
||||||
ver: String = "latest",
|
|
||||||
platform: Tool.Platform = Tool.Platform.ANDROID
|
suspend fun updateTool(toolEntity: ToolEntity)
|
||||||
): Flow<Result<Tool>>
|
|
||||||
|
suspend fun removeTool(toolEntity: ToolEntity)
|
||||||
}
|
}
|
||||||
@@ -9,24 +9,24 @@ import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
|
|||||||
import top.fatweb.oxygen.toolbox.data.tool.ToolDataSource
|
import top.fatweb.oxygen.toolbox.data.tool.ToolDataSource
|
||||||
import top.fatweb.oxygen.toolbox.model.Result
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
import top.fatweb.oxygen.toolbox.model.asExternalModel
|
import top.fatweb.oxygen.toolbox.model.asExternalModel
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
import top.fatweb.oxygen.toolbox.network.model.ToolBaseVo
|
import top.fatweb.oxygen.toolbox.network.model.ToolBaseVo
|
||||||
import top.fatweb.oxygen.toolbox.network.model.ToolVo
|
import top.fatweb.oxygen.toolbox.network.model.ToolVo
|
||||||
import top.fatweb.oxygen.toolbox.network.model.asExternalModel
|
import top.fatweb.oxygen.toolbox.network.model.asExternalModel
|
||||||
import top.fatweb.oxygen.toolbox.network.paging.ToolStorePagingSource
|
import top.fatweb.oxygen.toolbox.network.paging.ToolStorePagingSource
|
||||||
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val PAGE_SIZE = 20
|
private const val PAGE_SIZE = 20
|
||||||
|
|
||||||
internal class NetworkToolRepository @Inject constructor(
|
internal class NetworkStoreRepository @Inject constructor(
|
||||||
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
|
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
|
||||||
private val toolDataSource: ToolDataSource
|
private val toolDataSource: ToolDataSource
|
||||||
) : ToolRepository {
|
) : StoreRepository {
|
||||||
override val toolViewTemplate: Flow<String>
|
override val toolViewTemplate: Flow<String>
|
||||||
get() = toolDataSource.toolViewTemplate
|
get() = toolDataSource.toolViewTemplate
|
||||||
|
|
||||||
override suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<Tool>> =
|
override suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<ToolEntity>> =
|
||||||
Pager(
|
Pager(
|
||||||
config = PagingConfig(PAGE_SIZE),
|
config = PagingConfig(PAGE_SIZE),
|
||||||
pagingSourceFactory = { ToolStorePagingSource(oxygenNetworkDataSource, searchValue) }
|
pagingSourceFactory = { ToolStorePagingSource(oxygenNetworkDataSource, searchValue) }
|
||||||
@@ -36,8 +36,8 @@ internal class NetworkToolRepository @Inject constructor(
|
|||||||
username: String,
|
username: String,
|
||||||
toolId: String,
|
toolId: String,
|
||||||
ver: String,
|
ver: String,
|
||||||
platform: Tool.Platform
|
platform: ToolEntity.Platform
|
||||||
): Flow<Result<Tool>> =
|
): Flow<Result<ToolEntity>> =
|
||||||
oxygenNetworkDataSource.detail(
|
oxygenNetworkDataSource.detail(
|
||||||
username,
|
username,
|
||||||
toolId,
|
toolId,
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.repository.tool.impl
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.dao.ToolDao
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class OfflineToolRepository @Inject constructor(
|
||||||
|
private val toolDao: ToolDao
|
||||||
|
) : ToolRepository {
|
||||||
|
override fun getAllToolsStream(): Flow<List<ToolEntity>> =
|
||||||
|
toolDao.selectAllTools()
|
||||||
|
|
||||||
|
override fun getToolById(id: Long): Flow<ToolEntity?> =
|
||||||
|
toolDao.selectToolById(id)
|
||||||
|
|
||||||
|
override fun getToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?> =
|
||||||
|
toolDao.selectToolByUsernameAndToolId(username, toolId)
|
||||||
|
|
||||||
|
override suspend fun saveTool(toolEntity: ToolEntity) =
|
||||||
|
toolDao.insertTool(toolEntity)
|
||||||
|
|
||||||
|
override suspend fun updateTool(toolEntity: ToolEntity) =
|
||||||
|
toolDao.updateTool(toolEntity)
|
||||||
|
|
||||||
|
override suspend fun removeTool(toolEntity: ToolEntity) =
|
||||||
|
toolDao.deleteTool(toolEntity)
|
||||||
|
}
|
||||||
@@ -22,11 +22,13 @@ import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
|||||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||||
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
||||||
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
||||||
|
import top.fatweb.oxygen.toolbox.navigation.TOOL_STORE_ROUTE
|
||||||
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
||||||
import top.fatweb.oxygen.toolbox.navigation.navigateToAbout
|
import top.fatweb.oxygen.toolbox.navigation.navigateToAbout
|
||||||
import top.fatweb.oxygen.toolbox.navigation.navigateToLibraries
|
import top.fatweb.oxygen.toolbox.navigation.navigateToLibraries
|
||||||
import top.fatweb.oxygen.toolbox.navigation.navigateToSearch
|
import top.fatweb.oxygen.toolbox.navigation.navigateToSearch
|
||||||
import top.fatweb.oxygen.toolbox.navigation.navigateToStar
|
import top.fatweb.oxygen.toolbox.navigation.navigateToStar
|
||||||
|
import top.fatweb.oxygen.toolbox.navigation.navigateToToolStore
|
||||||
import top.fatweb.oxygen.toolbox.navigation.navigateToTools
|
import top.fatweb.oxygen.toolbox.navigation.navigateToTools
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ class OxygenAppState(
|
|||||||
|
|
||||||
val currentTopLevelDestination: TopLevelDestination?
|
val currentTopLevelDestination: TopLevelDestination?
|
||||||
@Composable get() = when (currentDestination?.route) {
|
@Composable get() = when (currentDestination?.route) {
|
||||||
|
TOOL_STORE_ROUTE -> TopLevelDestination.TOOL_STORE
|
||||||
TOOLS_ROUTE -> TopLevelDestination.TOOLS
|
TOOLS_ROUTE -> TopLevelDestination.TOOLS
|
||||||
STAR_ROUTE -> TopLevelDestination.STAR
|
STAR_ROUTE -> TopLevelDestination.STAR
|
||||||
else -> null
|
else -> null
|
||||||
@@ -110,6 +113,7 @@ class OxygenAppState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (topLevelDestination) {
|
when (topLevelDestination) {
|
||||||
|
TopLevelDestination.TOOL_STORE -> navController.navigateToToolStore(topLevelNavOptions)
|
||||||
TopLevelDestination.TOOLS -> navController.navigateToTools(topLevelNavOptions)
|
TopLevelDestination.TOOLS -> navController.navigateToTools(topLevelNavOptions)
|
||||||
TopLevelDestination.STAR -> navController.navigateToStar(topLevelNavOptions)
|
TopLevelDestination.STAR -> navController.navigateToStar(topLevelNavOptions)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui.component
|
package top.fatweb.oxygen.toolbox.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -14,47 +18,88 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import top.fatweb.oxygen.toolbox.R
|
import top.fatweb.oxygen.toolbox.R
|
||||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ToolCard(
|
fun ToolCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
tool: Tool,
|
tool: ToolEntity,
|
||||||
onClickToolCard: () -> Unit
|
actionIcon: ImageVector? = null,
|
||||||
|
actionIconContentDescription: String = "",
|
||||||
|
onAction: () -> Unit = {},
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
onLongClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier,
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
|
),
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
onClick = onClickToolCard
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
ToolVer(ver = tool.ver)
|
ToolHeader(
|
||||||
|
ver = tool.ver,
|
||||||
|
actionIcon = actionIcon,
|
||||||
|
actionIconContentDescription = actionIconContentDescription,
|
||||||
|
onAction = onAction
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ToolIcon(icon = tool.icon)
|
ToolIcon(icon = tool.icon)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ToolInfo(
|
ToolInfo(
|
||||||
toolName = tool.name,
|
toolName = tool.name,
|
||||||
toolId = tool.toolId,
|
toolId = tool.toolId,
|
||||||
toolDesc = tool.description ?: ""
|
toolDesc = tool.description
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
AuthorInfo(
|
AuthorInfo(
|
||||||
avatar = tool.author.avatar,
|
avatar = tool.authorAvatar,
|
||||||
nickname = tool.author.nickname
|
nickname = tool.authorNickname
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToolHeader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
ver: String,
|
||||||
|
actionIcon: ImageVector?,
|
||||||
|
actionIconContentDescription: String,
|
||||||
|
onAction: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.height(28.dp)
|
||||||
|
) {
|
||||||
|
ToolVer(ver = ver)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
actionIcon?.let {
|
||||||
|
ToolAction(
|
||||||
|
actionIcon = actionIcon,
|
||||||
|
actionIconContentDescription = actionIconContentDescription,
|
||||||
|
onAction = onAction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,13 +111,18 @@ fun ToolVer(
|
|||||||
ver: String
|
ver: String
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier,
|
modifier = modifier
|
||||||
|
.fillMaxHeight(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
colors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.onSecondaryContainer)
|
colors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
.background(color = MaterialTheme.colorScheme.surfaceContainer)
|
.background(color = MaterialTheme.colorScheme.surfaceContainer)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
Arrangement.Center,
|
||||||
|
Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
@@ -82,6 +132,38 @@ fun ToolVer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToolAction(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
actionIcon: ImageVector,
|
||||||
|
actionIconContentDescription: String,
|
||||||
|
onAction: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable(
|
||||||
|
onClick = onAction
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(color = MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(horizontal = 6.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier,
|
||||||
|
imageVector = actionIcon,
|
||||||
|
contentDescription = actionIconContentDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ToolIcon(
|
fun ToolIcon(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -105,7 +187,7 @@ fun ToolInfo(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
toolName: String,
|
toolName: String,
|
||||||
toolId: String,
|
toolId: String,
|
||||||
toolDesc: String
|
toolDesc: String?
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
@@ -121,13 +203,15 @@ fun ToolInfo(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
text = "ID: $toolId"
|
text = "ID: $toolId"
|
||||||
)
|
)
|
||||||
|
toolDesc?.let {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.outline,
|
color = MaterialTheme.colorScheme.outline,
|
||||||
text = "${stringResource(R.string.feature_tools_description)}: $toolDesc"
|
text = "${stringResource(R.string.feature_tools_description)}: $it"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.tool
|
||||||
|
|
||||||
|
import androidx.activity.compose.ReportDrawnWhen
|
||||||
|
import androidx.compose.animation.core.Ease
|
||||||
|
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.gestures.Orientation
|
||||||
|
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.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import top.fatweb.oxygen.toolbox.R
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.Loading
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.ToolCard
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ToolStoreRoute(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: ToolStoreViewModel = hiltViewModel(),
|
||||||
|
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||||
|
) {
|
||||||
|
val toolStorePagingItems = viewModel.storeData.collectAsLazyPagingItems()
|
||||||
|
val installInfo by viewModel.installInfo.collectAsState()
|
||||||
|
|
||||||
|
ToolStoreScreen(
|
||||||
|
modifier = modifier,
|
||||||
|
onNavigateToToolView = onNavigateToToolView,
|
||||||
|
toolStorePagingItems = toolStorePagingItems,
|
||||||
|
onChangeInstallStatus = viewModel::changeInstallStatus,
|
||||||
|
onInstallTool = viewModel::installTool,
|
||||||
|
installInfo = installInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ToolStoreScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||||
|
toolStorePagingItems: LazyPagingItems<ToolEntity>,
|
||||||
|
onChangeInstallStatus: (installStatus: ToolStoreUiState.Status, username: String?, toolId: String?) -> Unit,
|
||||||
|
onInstallTool: () -> Unit,
|
||||||
|
installInfo: ToolStoreUiState.InstallInfo
|
||||||
|
) {
|
||||||
|
val isToolLoading =
|
||||||
|
toolStorePagingItems.loadState.refresh is LoadState.Loading
|
||||||
|
|| toolStorePagingItems.loadState.append is LoadState.Loading
|
||||||
|
|
||||||
|
ReportDrawnWhen { !isToolLoading }
|
||||||
|
|
||||||
|
val itemsAvailable = toolStorePagingItems.itemCount
|
||||||
|
|
||||||
|
val state = rememberLazyStaggeredGridState()
|
||||||
|
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
|
||||||
|
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
LazyVerticalStaggeredGrid(
|
||||||
|
columns = StaggeredGridCells.Adaptive(160.dp),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalItemSpacing = 24.dp,
|
||||||
|
state = state
|
||||||
|
) {
|
||||||
|
toolsPanel(
|
||||||
|
toolStorePagingItems = toolStorePagingItems,
|
||||||
|
onAction = { username, toolId ->
|
||||||
|
onChangeInstallStatus(
|
||||||
|
ToolStoreUiState.Status.Pending,
|
||||||
|
username,
|
||||||
|
toolId
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onNavigateToToolView
|
||||||
|
)
|
||||||
|
|
||||||
|
item(span = StaggeredGridItemSpan.FullLine) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolStorePagingItems.loadState.refresh is LoadState.Loading || toolStorePagingItems.loadState.append is LoadState.Loading) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
val angle by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0F,
|
||||||
|
targetValue = 360F,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(800, easing = Ease),
|
||||||
|
), label = "angle"
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.graphicsLayer { rotationZ = angle },
|
||||||
|
imageVector = OxygenIcons.Loading,
|
||||||
|
contentDescription = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.DraggableScrollbar(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.windowInsetsPadding(WindowInsets.systemBars)
|
||||||
|
.padding(horizontal = 2.dp)
|
||||||
|
.align(Alignment.CenterEnd),
|
||||||
|
state = scrollbarState, orientation = Orientation.Vertical,
|
||||||
|
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installInfo.status != ToolStoreUiState.Status.None) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
if (installInfo.status == ToolStoreUiState.Status.Pending) {
|
||||||
|
onChangeInstallStatus(ToolStoreUiState.Status.None, null, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = when (installInfo.status) {
|
||||||
|
ToolStoreUiState.Status.Success -> OxygenIcons.Success
|
||||||
|
ToolStoreUiState.Status.Fail -> OxygenIcons.Error
|
||||||
|
else -> OxygenIcons.Info
|
||||||
|
},
|
||||||
|
contentDescription = stringResource(R.string.core_install)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
when (installInfo.status) {
|
||||||
|
ToolStoreUiState.Status.Success -> R.string.feature_store_install_success
|
||||||
|
ToolStoreUiState.Status.Fail -> R.string.feature_store_install_fail
|
||||||
|
else -> R.string.feature_store_install_tool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(360.dp)
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
when (installInfo.status) {
|
||||||
|
ToolStoreUiState.Status.Pending ->
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
R.string.feature_store_ask_install,
|
||||||
|
installInfo.username,
|
||||||
|
installInfo.toolId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ToolStoreUiState.Status.Installing ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(text = stringResource(R.string.core_installing))
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolStoreUiState.Status.Success ->
|
||||||
|
Text(text = stringResource(R.string.feature_store_install_success_info))
|
||||||
|
|
||||||
|
ToolStoreUiState.Status.Fail ->
|
||||||
|
Text(text = stringResource(R.string.feature_store_install_fail_info))
|
||||||
|
|
||||||
|
ToolStoreUiState.Status.None -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
if (installInfo.status == ToolStoreUiState.Status.Pending) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onChangeInstallStatus(ToolStoreUiState.Status.None, null, null)
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(R.string.core_cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
when (installInfo.status) {
|
||||||
|
ToolStoreUiState.Status.Pending ->
|
||||||
|
TextButton(onClick = onInstallTool) {
|
||||||
|
Text(text = stringResource(R.string.core_install))
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolStoreUiState.Status.Success,
|
||||||
|
ToolStoreUiState.Status.Fail ->
|
||||||
|
TextButton(onClick = {
|
||||||
|
onChangeInstallStatus(ToolStoreUiState.Status.None, null, null)
|
||||||
|
}) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
if (installInfo.status == ToolStoreUiState.Status.Success) R.string.core_ok
|
||||||
|
else R.string.core_close
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolStoreUiState.Status.None,
|
||||||
|
ToolStoreUiState.Status.Installing -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyStaggeredGridScope.toolsPanel(
|
||||||
|
toolStorePagingItems: LazyPagingItems<ToolEntity>,
|
||||||
|
onAction: (username: String, toolId: String) -> Unit,
|
||||||
|
onClick: (username: String, toolId: String) -> Unit
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = toolStorePagingItems.itemSnapshotList,
|
||||||
|
key = { it!!.id },
|
||||||
|
) {
|
||||||
|
ToolCard(
|
||||||
|
tool = it!!,
|
||||||
|
actionIcon = OxygenIcons.Download,
|
||||||
|
actionIconContentDescription = stringResource(R.string.core_install),
|
||||||
|
onAction = { onAction(it.authorUsername, it.toolId) },
|
||||||
|
onClick = { onClick(it.authorUsername, it.toolId) },
|
||||||
|
onLongClick = { onClick(it.authorUsername, it.toolId) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.tool
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ToolStoreViewModel @Inject constructor(
|
||||||
|
private val storeRepository: StoreRepository,
|
||||||
|
private val toolRepository: ToolRepository,
|
||||||
|
private val savedStateHandle: SavedStateHandle
|
||||||
|
) : ViewModel() {
|
||||||
|
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
|
||||||
|
private val currentPage = savedStateHandle.getStateFlow(CURRENT_PAGE, 1)
|
||||||
|
val installInfo = savedStateHandle.getStateFlow(
|
||||||
|
INSTALL_INFO, ToolStoreUiState.InstallInfo()
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val storeData: Flow<PagingData<ToolEntity>> = combine(
|
||||||
|
searchValue, currentPage, ::Pair
|
||||||
|
).flatMapLatest { (searchValue, currentPage) ->
|
||||||
|
storeRepository.getStore(searchValue, currentPage).cachedIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeInstallStatus(
|
||||||
|
installStatus: ToolStoreUiState.Status, username: String?, toolId: String?
|
||||||
|
) {
|
||||||
|
savedStateHandle[INSTALL_INFO] =
|
||||||
|
ToolStoreUiState.InstallInfo(installStatus, username ?: "Unknown", toolId ?: "Unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installTool() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val (_, username, toolId) = installInfo.value
|
||||||
|
storeRepository.detail(username, toolId).collect {
|
||||||
|
when (it) {
|
||||||
|
Result.Loading -> savedStateHandle[INSTALL_INFO] =
|
||||||
|
ToolStoreUiState.InstallInfo(ToolStoreUiState.Status.Installing)
|
||||||
|
|
||||||
|
is Result.Error, is Result.Fail -> savedStateHandle[INSTALL_INFO] =
|
||||||
|
ToolStoreUiState.InstallInfo(ToolStoreUiState.Status.Fail)
|
||||||
|
|
||||||
|
is Result.Success -> {
|
||||||
|
toolRepository.saveTool(it.data)
|
||||||
|
savedStateHandle[INSTALL_INFO] =
|
||||||
|
ToolStoreUiState.InstallInfo(ToolStoreUiState.Status.Success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ToolStoreUiState(
|
||||||
|
val installInfo: InstallInfo
|
||||||
|
) : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data class InstallInfo(
|
||||||
|
var status: Status = Status.None,
|
||||||
|
var username: String = "Unknown",
|
||||||
|
var toolId: String = "Unknown"
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
enum class Status {
|
||||||
|
None, Pending, Installing, Success, Fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SEARCH_VALUE = "searchValue"
|
||||||
|
private const val CURRENT_PAGE = "currentPage"
|
||||||
|
private const val INSTALL_INFO = "installInfo"
|
||||||
@@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import top.fatweb.oxygen.toolbox.model.Result
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
import top.fatweb.oxygen.toolbox.navigation.ToolViewArgs
|
import top.fatweb.oxygen.toolbox.navigation.ToolViewArgs
|
||||||
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
|
||||||
import top.fatweb.oxygen.toolbox.util.decodeToStringWithZip
|
import top.fatweb.oxygen.toolbox.util.decodeToStringWithZip
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
@@ -22,7 +22,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ToolViewScreenViewModel @Inject constructor(
|
class ToolViewScreenViewModel @Inject constructor(
|
||||||
toolRepository: ToolRepository,
|
storeRepository: StoreRepository,
|
||||||
savedStateHandle: SavedStateHandle
|
savedStateHandle: SavedStateHandle
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val toolViewArgs = ToolViewArgs(savedStateHandle)
|
private val toolViewArgs = ToolViewArgs(savedStateHandle)
|
||||||
@@ -32,7 +32,7 @@ class ToolViewScreenViewModel @Inject constructor(
|
|||||||
val toolViewUiState: StateFlow<ToolViewUiState> = toolViewUiState(
|
val toolViewUiState: StateFlow<ToolViewUiState> = toolViewUiState(
|
||||||
username = username,
|
username = username,
|
||||||
toolId = toolId,
|
toolId = toolId,
|
||||||
toolRepository = toolRepository
|
storeRepository = storeRepository
|
||||||
)
|
)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
@@ -44,13 +44,13 @@ class ToolViewScreenViewModel @Inject constructor(
|
|||||||
private fun toolViewUiState(
|
private fun toolViewUiState(
|
||||||
username: String,
|
username: String,
|
||||||
toolId: String,
|
toolId: String,
|
||||||
toolRepository: ToolRepository
|
storeRepository: StoreRepository
|
||||||
): Flow<ToolViewUiState> {
|
): Flow<ToolViewUiState> {
|
||||||
val result = toolRepository.detail(
|
val result = storeRepository.detail(
|
||||||
username = username,
|
username = username,
|
||||||
toolId = toolId
|
toolId = toolId
|
||||||
)
|
)
|
||||||
val toolViewTemplate = toolRepository.toolViewTemplate
|
val toolViewTemplate = storeRepository.toolViewTemplate
|
||||||
|
|
||||||
return combine(result, toolViewTemplate, ::Pair).map { (result, toolViewTemplate) ->
|
return combine(result, toolViewTemplate, ::Pair).map { (result, toolViewTemplate) ->
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -87,7 +87,7 @@ sealed interface ToolViewUiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
fun processHtml(toolViewTemplate: String, distBase64: String, baseBase64: String): String {
|
private fun processHtml(toolViewTemplate: String, distBase64: String, baseBase64: String): String {
|
||||||
val dist = Base64.decodeToStringWithZip(distBase64)
|
val dist = Base64.decodeToStringWithZip(distBase64)
|
||||||
val base = Base64.decodeToStringWithZip(baseBase64)
|
val base = Base64.decodeToStringWithZip(baseBase64)
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui.tool
|
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.items
|
|
||||||
import androidx.paging.compose.LazyPagingItems
|
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
|
||||||
import top.fatweb.oxygen.toolbox.ui.component.ToolCard
|
|
||||||
|
|
||||||
fun LazyStaggeredGridScope.toolsPanel(
|
|
||||||
toolStorePagingItems: LazyPagingItems<Tool>,
|
|
||||||
onClickToolCard: (username: String, toolId: String) -> Unit
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = toolStorePagingItems.itemSnapshotList,
|
|
||||||
key = { it!!.id },
|
|
||||||
) {
|
|
||||||
ToolCard(
|
|
||||||
tool = it!!,
|
|
||||||
onClickToolCard = {onClickToolCard(it.author.username, it.toolId)}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,25 +23,29 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.paging.LoadState
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import top.fatweb.oxygen.toolbox.R
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
|
||||||
import top.fatweb.oxygen.toolbox.icon.Loading
|
import top.fatweb.oxygen.toolbox.icon.Loading
|
||||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.ToolCard
|
||||||
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar
|
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar
|
||||||
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller
|
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller
|
||||||
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
|
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
|
||||||
@@ -51,14 +55,15 @@ internal fun ToolsRoute(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: ToolsScreenViewModel = hiltViewModel(),
|
viewModel: ToolsScreenViewModel = hiltViewModel(),
|
||||||
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||||
onShowSnackbar: suspend (String, String?) -> Boolean
|
onNavigateToToolStore: () -> Unit
|
||||||
) {
|
) {
|
||||||
val toolStorePagingItems = viewModel.storeData.collectAsLazyPagingItems()
|
val toolsScreenUiStateState by viewModel.toolsScreenUiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
ToolsScreen(
|
ToolsScreen(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onNavigateToToolView = onNavigateToToolView,
|
onNavigateToToolView = onNavigateToToolView,
|
||||||
toolStorePagingItems = toolStorePagingItems
|
onNavigateToToolStore = onNavigateToToolStore,
|
||||||
|
toolsScreenUiState = toolsScreenUiStateState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,15 +71,12 @@ internal fun ToolsRoute(
|
|||||||
internal fun ToolsScreen(
|
internal fun ToolsScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||||
toolStorePagingItems: LazyPagingItems<Tool>
|
onNavigateToToolStore: () -> Unit,
|
||||||
|
toolsScreenUiState: ToolsScreenUiState
|
||||||
) {
|
) {
|
||||||
val isToolLoading =
|
ReportDrawnWhen { toolsScreenUiState !is ToolsScreenUiState.Loading }
|
||||||
toolStorePagingItems.loadState.refresh is LoadState.Loading
|
|
||||||
|| toolStorePagingItems.loadState.append is LoadState.Loading
|
|
||||||
|
|
||||||
ReportDrawnWhen { !isToolLoading }
|
val itemsAvailable = howManyTools(toolsScreenUiState)
|
||||||
|
|
||||||
val itemsAvailable = toolStorePagingItems.itemCount
|
|
||||||
|
|
||||||
val state = rememberLazyStaggeredGridState()
|
val state = rememberLazyStaggeredGridState()
|
||||||
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
|
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
|
||||||
@@ -84,26 +86,9 @@ internal fun ToolsScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier.fillMaxSize()
|
modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
LazyVerticalStaggeredGrid(
|
|
||||||
columns = StaggeredGridCells.Adaptive(160.dp),
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
verticalItemSpacing = 24.dp,
|
|
||||||
state = state
|
|
||||||
) {
|
|
||||||
|
|
||||||
toolsPanel(
|
when (toolsScreenUiState) {
|
||||||
toolStorePagingItems = toolStorePagingItems,
|
ToolsScreenUiState.Loading -> {
|
||||||
onClickToolCard = onNavigateToToolView
|
|
||||||
)
|
|
||||||
|
|
||||||
item(span = StaggeredGridItemSpan.FullLine) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolStorePagingItems.loadState.refresh is LoadState.Loading || toolStorePagingItems.loadState.append is LoadState.Loading) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -125,6 +110,39 @@ internal fun ToolsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ToolsScreenUiState.Nothing -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.feature_tools_no_tools_installed))
|
||||||
|
TextButton(onClick = onNavigateToToolStore) {
|
||||||
|
Text(text = stringResource(R.string.feature_tools_go_to_store))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ToolsScreenUiState.Success -> {
|
||||||
|
LazyVerticalStaggeredGrid(
|
||||||
|
columns = StaggeredGridCells.Adaptive(160.dp),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalItemSpacing = 24.dp,
|
||||||
|
state = state
|
||||||
|
) {
|
||||||
|
|
||||||
|
toolsPanel(
|
||||||
|
toolItems = toolsScreenUiState.tools,
|
||||||
|
onClickToolCard = onNavigateToToolView
|
||||||
|
)
|
||||||
|
|
||||||
|
item(span = StaggeredGridItemSpan.FullLine) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.DraggableScrollbar(
|
state.DraggableScrollbar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -137,3 +155,26 @@ internal fun ToolsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun LazyStaggeredGridScope.toolsPanel(
|
||||||
|
toolItems: List<ToolEntity>,
|
||||||
|
onClickToolCard: (username: String, toolId: String) -> Unit
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = toolItems,
|
||||||
|
key = { it.id },
|
||||||
|
) {
|
||||||
|
ToolCard(
|
||||||
|
tool = it,
|
||||||
|
onClick = {onClickToolCard(it.authorUsername, it.toolId)},
|
||||||
|
onLongClick = {onClickToolCard(it.authorUsername, it.toolId)}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun howManyTools(toolsScreenUiState: ToolsScreenUiState) =
|
||||||
|
when (toolsScreenUiState) {
|
||||||
|
ToolsScreenUiState.Loading, ToolsScreenUiState.Nothing -> 0
|
||||||
|
is ToolsScreenUiState.Success -> toolsScreenUiState.tools.size
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,40 +3,46 @@ package top.fatweb.oxygen.toolbox.ui.tool
|
|||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.PagingData
|
|
||||||
import androidx.paging.cachedIn
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import top.fatweb.oxygen.toolbox.model.Page
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
|
||||||
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ToolsScreenViewModel @Inject constructor(
|
class ToolsScreenViewModel @Inject constructor(
|
||||||
private val toolRepository: ToolRepository,
|
private val storeRepository: StoreRepository,
|
||||||
|
toolRepository: ToolRepository,
|
||||||
savedStateHandle: SavedStateHandle
|
savedStateHandle: SavedStateHandle
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
|
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
|
||||||
private val currentPage = savedStateHandle.getStateFlow(CURRENT_PAGE, 1)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
val toolsScreenUiState: StateFlow<ToolsScreenUiState> =
|
||||||
val storeData: Flow<PagingData<Tool>> = combine(
|
toolRepository.getAllToolsStream()
|
||||||
searchValue,
|
.map {
|
||||||
currentPage,
|
if (it.isEmpty()) {
|
||||||
::Pair
|
ToolsScreenUiState.Nothing
|
||||||
).flatMapLatest { (searchValue, currentPage) ->
|
} else {
|
||||||
toolRepository.getStore(searchValue, currentPage).cachedIn(viewModelScope)
|
ToolsScreenUiState.Success(it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
initialValue = ToolsScreenUiState.Loading,
|
||||||
|
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ToolsScreenUiState {
|
sealed interface ToolsScreenUiState {
|
||||||
data object Loading : ToolsScreenUiState
|
data object Loading : ToolsScreenUiState
|
||||||
data class Success(val tools: Page<Tool>) : ToolsScreenUiState
|
data object Nothing : ToolsScreenUiState
|
||||||
|
data class Success(val tools: List<ToolEntity>) : ToolsScreenUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val SEARCH_VALUE = "searchValue"
|
private const val SEARCH_VALUE = "searchValue"
|
||||||
private const val CURRENT_PAGE = "currentPage"
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="app_name">氧工具</string>
|
<string name="app_name">氧工具</string>
|
||||||
<string name="app_full_name">氧工具</string>
|
<string name="app_full_name">氧工具</string>
|
||||||
<string name="app_description">All in One</string>
|
<string name="app_description">All in One</string>
|
||||||
|
|
||||||
<string name="core_ok">完成</string>
|
<string name="core_ok">完成</string>
|
||||||
<string name="core_back">返回</string>
|
<string name="core_back">返回</string>
|
||||||
<string name="core_close">关闭</string>
|
<string name="core_close">关闭</string>
|
||||||
@@ -11,8 +12,26 @@
|
|||||||
<string name="core_search">搜索</string>
|
<string name="core_search">搜索</string>
|
||||||
<string name="core_loading">加载中…</string>
|
<string name="core_loading">加载中…</string>
|
||||||
<string name="core_no_connect">⚠️ 无法连接至互联网</string>
|
<string name="core_no_connect">⚠️ 无法连接至互联网</string>
|
||||||
|
<string name="core_install">安装</string>
|
||||||
|
<string name="core_installing">安装中……</string>
|
||||||
|
<string name="core_cancel">取消</string>
|
||||||
|
|
||||||
|
<string name="feature_store_title">商店</string>
|
||||||
|
<string name="feature_store_install_tool">安装工具</string>
|
||||||
|
<string name="feature_store_ask_install">确定安装由用户 %1$s 提供的工具 %2$s 吗?</string>
|
||||||
|
<string name="feature_store_install_success">安装成功</string>
|
||||||
|
<string name="feature_store_install_success_info">恭喜!工具安装成功。</string>
|
||||||
|
<string name="feature_store_install_fail">安装失败</string>
|
||||||
|
<string name="feature_store_install_fail_info">安装失败!请稍后重试……</string>
|
||||||
|
|
||||||
<string name="feature_tools_title">工具</string>
|
<string name="feature_tools_title">工具</string>
|
||||||
|
<string name="feature_tools_description">简介</string>
|
||||||
|
<string name="feature_tools_can_not_open">⚠️ 无法打开工具</string>
|
||||||
|
<string name="feature_tools_no_tools_installed">暂无工具已安装</string>
|
||||||
|
<string name="feature_tools_go_to_store">前往商店…</string>
|
||||||
|
|
||||||
<string name="feature_star_title">收藏</string>
|
<string name="feature_star_title">收藏</string>
|
||||||
|
|
||||||
<string name="feature_settings_title">设置</string>
|
<string name="feature_settings_title">设置</string>
|
||||||
<string name="feature_settings_language">语言</string>
|
<string name="feature_settings_language">语言</string>
|
||||||
<string name="feature_settings_language_system_default">系统默认</string>
|
<string name="feature_settings_language_system_default">系统默认</string>
|
||||||
@@ -34,6 +53,4 @@
|
|||||||
<string name="feature_settings_more_about">关于</string>
|
<string name="feature_settings_more_about">关于</string>
|
||||||
<string name="feature_settings_top_app_bar_action_icon_description">更多</string>
|
<string name="feature_settings_top_app_bar_action_icon_description">更多</string>
|
||||||
<string name="feature_settings_top_app_bar_navigation_icon_description">搜索</string>
|
<string name="feature_settings_top_app_bar_navigation_icon_description">搜索</string>
|
||||||
<string name="feature_tools_description">简介</string>
|
|
||||||
<string name="feature_tools_can_not_open">⚠️ 无法打开工具</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<string name="app_name">Oxygen</string>
|
<string name="app_name">Oxygen</string>
|
||||||
<string name="app_full_name">Oxygen Toolbox</string>
|
<string name="app_full_name">Oxygen Toolbox</string>
|
||||||
<string name="app_description">All in One</string>
|
<string name="app_description">All in One</string>
|
||||||
|
|
||||||
<string name="core_ok">OK</string>
|
<string name="core_ok">OK</string>
|
||||||
<string name="core_back">Back</string>
|
<string name="core_back">Back</string>
|
||||||
<string name="core_close">Close</string>
|
<string name="core_close">Close</string>
|
||||||
@@ -10,8 +11,26 @@
|
|||||||
<string name="core_search">Search</string>
|
<string name="core_search">Search</string>
|
||||||
<string name="core_loading">Loading…</string>
|
<string name="core_loading">Loading…</string>
|
||||||
<string name="core_no_connect">⚠️ Unable to connect to the internet</string>
|
<string name="core_no_connect">⚠️ Unable to connect to the internet</string>
|
||||||
|
<string name="core_install">Install</string>
|
||||||
|
<string name="core_installing">Installing…</string>
|
||||||
|
<string name="core_cancel">Cancel</string>
|
||||||
|
|
||||||
|
<string name="feature_store_title">Store</string>
|
||||||
|
<string name="feature_store_install_tool">Install Tool</string>
|
||||||
|
<string name="feature_store_ask_install">Are you sure to install tool %1$s provided by user %2$s?</string>
|
||||||
|
<string name="feature_store_install_success">Install Success</string>
|
||||||
|
<string name="feature_store_install_success_info">Congratulations, the tool installation is successful.</string>
|
||||||
|
<string name="feature_store_install_fail">Install Failed</string>
|
||||||
|
<string name="feature_store_install_fail_info">Installation failed, please try again later…</string>
|
||||||
|
|
||||||
<string name="feature_tools_title">Tools</string>
|
<string name="feature_tools_title">Tools</string>
|
||||||
|
<string name="feature_tools_description">Desc</string>
|
||||||
|
<string name="feature_tools_can_not_open">⚠️ Can not open the tool</string>
|
||||||
|
<string name="feature_tools_no_tools_installed">No tools installed yet</string>
|
||||||
|
<string name="feature_tools_go_to_store">Go to store…</string>
|
||||||
|
|
||||||
<string name="feature_star_title">Star</string>
|
<string name="feature_star_title">Star</string>
|
||||||
|
|
||||||
<string name="feature_settings_title">Settings</string>
|
<string name="feature_settings_title">Settings</string>
|
||||||
<string name="feature_settings_language">Language</string>
|
<string name="feature_settings_language">Language</string>
|
||||||
<string name="feature_settings_language_system_default">System Default</string>
|
<string name="feature_settings_language_system_default">System Default</string>
|
||||||
@@ -35,6 +54,4 @@
|
|||||||
<string name="feature_settings_more_about">About</string>
|
<string name="feature_settings_more_about">About</string>
|
||||||
<string name="feature_settings_top_app_bar_action_icon_description">More</string>
|
<string name="feature_settings_top_app_bar_action_icon_description">More</string>
|
||||||
<string name="feature_settings_top_app_bar_navigation_icon_description">Search</string>
|
<string name="feature_settings_top_app_bar_navigation_icon_description">Search</string>
|
||||||
<string name="feature_tools_description">Desc</string>
|
|
||||||
<string name="feature_tools_can_not_open">⚠️ Can not open the tool</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -9,4 +9,6 @@ plugins {
|
|||||||
alias(libs.plugins.protobuf) apply false
|
alias(libs.plugins.protobuf) apply false
|
||||||
alias(libs.plugins.kotlinxSerialization) apply false
|
alias(libs.plugins.kotlinxSerialization) apply false
|
||||||
alias(libs.plugins.secrets) apply false
|
alias(libs.plugins.secrets) apply false
|
||||||
|
alias(libs.plugins.room) apply false
|
||||||
|
alias(libs.plugins.parcelize) apply false
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ org.gradle.jvmargs=-Xmx2g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
|||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
|
|
||||||
# Enable caching between builds.
|
# Enable caching between builds.
|
||||||
org.gradle.caching=false
|
org.gradle.caching=true
|
||||||
|
|
||||||
# Enable configuration caching between builds.
|
# Enable configuration caching between builds.
|
||||||
org.gradle.configuration-cache=true
|
org.gradle.configuration-cache=true
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ room = "2.6.1"
|
|||||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin"}
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
|
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
|
||||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||||
|
|||||||
Reference in New Issue
Block a user