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.kotlinxSerialization)
|
||||
alias(libs.plugins.secrets)
|
||||
alias(libs.plugins.room)
|
||||
alias(libs.plugins.parcelize)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -120,6 +122,14 @@ secrets {
|
||||
defaultPropertiesFileName = "secrets.defaults.properties"
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.generateKotlin", "true")
|
||||
}
|
||||
|
||||
room {
|
||||
schemaDirectory("$projectDir/schemas")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
@@ -171,4 +181,7 @@ dependencies {
|
||||
implementation(libs.paging.compose)
|
||||
implementation(libs.androidsvg.aar)
|
||||
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.repository.lib.DepRepository
|
||||
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.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.impl.LocalUserDataRepository
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@@ -28,8 +30,11 @@ abstract class DataModule {
|
||||
internal abstract fun bindsUserDataRepository(userDataRepository: LocalUserDataRepository): UserDataRepository
|
||||
|
||||
@Binds
|
||||
internal abstract fun bindsToolRepository(toolRepository: NetworkToolRepository): ToolRepository
|
||||
internal abstract fun bindsDepRepository(depRepository: LocalDepRepository): DepRepository
|
||||
|
||||
@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.filled.AccessTime
|
||||
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.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.MoreVert
|
||||
import androidx.compose.material.icons.filled.Reorder
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
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.CheckCircle
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
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.asImageBitmap
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
@@ -33,6 +39,8 @@ object OxygenIcons {
|
||||
val Box = Icons.Default.Inbox
|
||||
val Close = Icons.Default.Close
|
||||
val Code = Icons.Default.Code
|
||||
val Download = Icons.Default.Download
|
||||
val Error = Icons.Default.Cancel
|
||||
val Home = Icons.Rounded.Home
|
||||
val HomeBorder = Icons.Outlined.Home
|
||||
val Info = Icons.Outlined.Info
|
||||
@@ -41,6 +49,9 @@ object OxygenIcons {
|
||||
val Search = Icons.Rounded.Search
|
||||
val Star = Icons.Rounded.Star
|
||||
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 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
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
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 name: String,
|
||||
@@ -17,7 +25,11 @@ data class Tool(
|
||||
|
||||
val base: String? = null,
|
||||
|
||||
val author: Author,
|
||||
val authorUsername: String,
|
||||
|
||||
val authorNickname: String,
|
||||
|
||||
val authorAvatar: String,
|
||||
|
||||
val ver: String,
|
||||
|
||||
@@ -33,7 +45,13 @@ data class Tool(
|
||||
|
||||
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 {
|
||||
WEB,
|
||||
@@ -42,12 +60,4 @@ data class Tool(
|
||||
|
||||
ANDROID
|
||||
}
|
||||
|
||||
data class Author(
|
||||
val username: String,
|
||||
|
||||
val nickname: String,
|
||||
|
||||
val avatar: String
|
||||
)
|
||||
}
|
||||
@@ -10,5 +10,5 @@ data class ToolGroup(
|
||||
|
||||
val title: String,
|
||||
|
||||
val tools: List<Tool> = emptyList()
|
||||
val tools: List<ToolEntity> = emptyList()
|
||||
)
|
||||
@@ -28,9 +28,12 @@ fun OxygenNavHost(
|
||||
librariesScreen(
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
toolStoreScreen(
|
||||
onNavigateToToolView = navController::navigateToToolView
|
||||
)
|
||||
toolsScreen(
|
||||
onNavigateToToolView = navController::navigateToToolView,
|
||||
onShowSnackbar = onShowSnackbar
|
||||
onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.TOOL_STORE) }
|
||||
)
|
||||
toolViewScreen(
|
||||
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(
|
||||
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||
onShowSnackbar: suspend (String, String?) -> Boolean
|
||||
onNavigateToToolStore: () -> Unit
|
||||
) {
|
||||
composable(
|
||||
route = TOOLS_ROUTE
|
||||
) {
|
||||
ToolsRoute(
|
||||
onNavigateToToolView = onNavigateToToolView,
|
||||
onShowSnackbar = onShowSnackbar
|
||||
onNavigateToToolStore = onNavigateToToolStore
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,13 @@ enum class TopLevelDestination(
|
||||
@StringRes val iconTextId: 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(
|
||||
selectedIcon = OxygenIcons.Home,
|
||||
unselectedIcon = OxygenIcons.HomeBorder,
|
||||
|
||||
@@ -3,7 +3,7 @@ package top.fatweb.oxygen.toolbox.network.model
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
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
|
||||
|
||||
@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.serialization.SerialName
|
||||
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
|
||||
|
||||
@Serializable
|
||||
@@ -62,7 +62,7 @@ data class ToolVo(
|
||||
}
|
||||
}
|
||||
|
||||
fun ToolVo.asExternalModel() = Tool(
|
||||
fun ToolVo.asExternalModel() = ToolEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
toolId = toolId,
|
||||
@@ -70,7 +70,9 @@ fun ToolVo.asExternalModel() = Tool(
|
||||
platform = platform.asExternalModel(),
|
||||
description = description,
|
||||
base = base?.dist?.data,
|
||||
author = author.asExternalModel(),
|
||||
authorUsername = author.username,
|
||||
authorNickname = author.userInfo.nickname,
|
||||
authorAvatar = author.userInfo.avatar,
|
||||
ver = ver,
|
||||
keywords = keywords,
|
||||
categories = categories.map { it.name },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package top.fatweb.oxygen.toolbox.network.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
||||
|
||||
@Serializable
|
||||
data class UserWithInfoVo(
|
||||
@@ -20,9 +19,3 @@ data class UserWithInfoVo(
|
||||
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.PagingState
|
||||
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.asExternalModel
|
||||
|
||||
internal class ToolStorePagingSource(
|
||||
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
|
||||
private val searchValue: String
|
||||
) : PagingSource<Int, Tool>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, Tool>): Int? = null
|
||||
) : PagingSource<Int, ToolEntity>() {
|
||||
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 {
|
||||
val currentPage = params.key ?: 1
|
||||
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
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import top.fatweb.oxygen.toolbox.model.Result
|
||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
||||
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||
|
||||
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(
|
||||
username: String,
|
||||
toolId: String,
|
||||
ver: String = "latest",
|
||||
platform: Tool.Platform = Tool.Platform.ANDROID
|
||||
): Flow<Result<Tool>>
|
||||
fun getToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?>
|
||||
|
||||
suspend fun saveTool(toolEntity: ToolEntity)
|
||||
|
||||
suspend fun updateTool(toolEntity: ToolEntity)
|
||||
|
||||
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.model.Result
|
||||
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.ToolVo
|
||||
import top.fatweb.oxygen.toolbox.network.model.asExternalModel
|
||||
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
|
||||
|
||||
private const val PAGE_SIZE = 20
|
||||
|
||||
internal class NetworkToolRepository @Inject constructor(
|
||||
internal class NetworkStoreRepository @Inject constructor(
|
||||
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
|
||||
private val toolDataSource: ToolDataSource
|
||||
) : ToolRepository {
|
||||
) : StoreRepository {
|
||||
override val toolViewTemplate: Flow<String>
|
||||
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(
|
||||
config = PagingConfig(PAGE_SIZE),
|
||||
pagingSourceFactory = { ToolStorePagingSource(oxygenNetworkDataSource, searchValue) }
|
||||
@@ -36,8 +36,8 @@ internal class NetworkToolRepository @Inject constructor(
|
||||
username: String,
|
||||
toolId: String,
|
||||
ver: String,
|
||||
platform: Tool.Platform
|
||||
): Flow<Result<Tool>> =
|
||||
platform: ToolEntity.Platform
|
||||
): Flow<Result<ToolEntity>> =
|
||||
oxygenNetworkDataSource.detail(
|
||||
username,
|
||||
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.navigation.STAR_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.navigateToAbout
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToLibraries
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToSearch
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToStar
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToToolStore
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToTools
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -73,6 +75,7 @@ class OxygenAppState(
|
||||
|
||||
val currentTopLevelDestination: TopLevelDestination?
|
||||
@Composable get() = when (currentDestination?.route) {
|
||||
TOOL_STORE_ROUTE -> TopLevelDestination.TOOL_STORE
|
||||
TOOLS_ROUTE -> TopLevelDestination.TOOLS
|
||||
STAR_ROUTE -> TopLevelDestination.STAR
|
||||
else -> null
|
||||
@@ -110,6 +113,7 @@ class OxygenAppState(
|
||||
}
|
||||
|
||||
when (topLevelDestination) {
|
||||
TopLevelDestination.TOOL_STORE -> navController.navigateToToolStore(topLevelNavOptions)
|
||||
TopLevelDestination.TOOLS -> navController.navigateToTools(topLevelNavOptions)
|
||||
TopLevelDestination.STAR -> navController.navigateToStar(topLevelNavOptions)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.component
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.fatweb.oxygen.toolbox.R
|
||||
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
|
||||
fun ToolCard(
|
||||
modifier: Modifier = Modifier,
|
||||
tool: Tool,
|
||||
onClickToolCard: () -> Unit
|
||||
tool: ToolEntity,
|
||||
actionIcon: ImageVector? = null,
|
||||
actionIconContentDescription: String = "",
|
||||
onAction: () -> Unit = {},
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {}
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
onClick = onClickToolCard
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Column(
|
||||
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))
|
||||
ToolIcon(icon = tool.icon)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ToolInfo(
|
||||
toolName = tool.name,
|
||||
toolId = tool.toolId,
|
||||
toolDesc = tool.description ?: ""
|
||||
toolDesc = tool.description
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AuthorInfo(
|
||||
avatar = tool.author.avatar,
|
||||
nickname = tool.author.nickname
|
||||
avatar = tool.authorAvatar,
|
||||
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
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.fillMaxHeight(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.background(color = MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
Arrangement.Center,
|
||||
Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
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
|
||||
fun ToolIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -105,7 +187,7 @@ fun ToolInfo(
|
||||
modifier: Modifier = Modifier,
|
||||
toolName: String,
|
||||
toolId: String,
|
||||
toolDesc: String
|
||||
toolDesc: String?
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
@@ -121,12 +203,14 @@ fun ToolInfo(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = "ID: $toolId"
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
text = "${stringResource(R.string.feature_tools_description)}: $toolDesc"
|
||||
)
|
||||
toolDesc?.let {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
text = "${stringResource(R.string.feature_tools_description)}: $it"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 top.fatweb.oxygen.toolbox.model.Result
|
||||
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 javax.inject.Inject
|
||||
import kotlin.io.encoding.Base64
|
||||
@@ -22,7 +22,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class ToolViewScreenViewModel @Inject constructor(
|
||||
toolRepository: ToolRepository,
|
||||
storeRepository: StoreRepository,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
private val toolViewArgs = ToolViewArgs(savedStateHandle)
|
||||
@@ -32,7 +32,7 @@ class ToolViewScreenViewModel @Inject constructor(
|
||||
val toolViewUiState: StateFlow<ToolViewUiState> = toolViewUiState(
|
||||
username = username,
|
||||
toolId = toolId,
|
||||
toolRepository = toolRepository
|
||||
storeRepository = storeRepository
|
||||
)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
@@ -44,13 +44,13 @@ class ToolViewScreenViewModel @Inject constructor(
|
||||
private fun toolViewUiState(
|
||||
username: String,
|
||||
toolId: String,
|
||||
toolRepository: ToolRepository
|
||||
storeRepository: StoreRepository
|
||||
): Flow<ToolViewUiState> {
|
||||
val result = toolRepository.detail(
|
||||
val result = storeRepository.detail(
|
||||
username = username,
|
||||
toolId = toolId
|
||||
)
|
||||
val toolViewTemplate = toolRepository.toolViewTemplate
|
||||
val toolViewTemplate = storeRepository.toolViewTemplate
|
||||
|
||||
return combine(result, toolViewTemplate, ::Pair).map { (result, toolViewTemplate) ->
|
||||
when (result) {
|
||||
@@ -87,7 +87,7 @@ sealed interface ToolViewUiState {
|
||||
}
|
||||
|
||||
@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 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.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.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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.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.rememberDraggableScroller
|
||||
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
|
||||
@@ -51,14 +55,15 @@ internal fun ToolsRoute(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ToolsScreenViewModel = hiltViewModel(),
|
||||
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(
|
||||
modifier = modifier,
|
||||
onNavigateToToolView = onNavigateToToolView,
|
||||
toolStorePagingItems = toolStorePagingItems
|
||||
onNavigateToToolStore = onNavigateToToolStore,
|
||||
toolsScreenUiState = toolsScreenUiStateState
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,15 +71,12 @@ internal fun ToolsRoute(
|
||||
internal fun ToolsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||
toolStorePagingItems: LazyPagingItems<Tool>
|
||||
onNavigateToToolStore: () -> Unit,
|
||||
toolsScreenUiState: ToolsScreenUiState
|
||||
) {
|
||||
val isToolLoading =
|
||||
toolStorePagingItems.loadState.refresh is LoadState.Loading
|
||||
|| toolStorePagingItems.loadState.append is LoadState.Loading
|
||||
ReportDrawnWhen { toolsScreenUiState !is ToolsScreenUiState.Loading }
|
||||
|
||||
ReportDrawnWhen { !isToolLoading }
|
||||
|
||||
val itemsAvailable = toolStorePagingItems.itemCount
|
||||
val itemsAvailable = howManyTools(toolsScreenUiState)
|
||||
|
||||
val state = rememberLazyStaggeredGridState()
|
||||
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
|
||||
@@ -84,45 +86,61 @@ internal fun ToolsScreen(
|
||||
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,
|
||||
onClickToolCard = onNavigateToToolView
|
||||
)
|
||||
|
||||
item(span = StaggeredGridItemSpan.FullLine) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
||||
when (toolsScreenUiState) {
|
||||
ToolsScreenUiState.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 = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
) {
|
||||
|
||||
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 = ""
|
||||
)
|
||||
toolsPanel(
|
||||
toolItems = toolsScreenUiState.tools,
|
||||
onClickToolCard = onNavigateToToolView
|
||||
)
|
||||
|
||||
item(span = StaggeredGridItemSpan.FullLine) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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 top.fatweb.oxygen.toolbox.model.Page
|
||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
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
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class ToolsScreenViewModel @Inject constructor(
|
||||
private val toolRepository: ToolRepository,
|
||||
private val storeRepository: StoreRepository,
|
||||
toolRepository: ToolRepository,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
|
||||
private val currentPage = savedStateHandle.getStateFlow(CURRENT_PAGE, 1)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val storeData: Flow<PagingData<Tool>> = combine(
|
||||
searchValue,
|
||||
currentPage,
|
||||
::Pair
|
||||
).flatMapLatest { (searchValue, currentPage) ->
|
||||
toolRepository.getStore(searchValue, currentPage).cachedIn(viewModelScope)
|
||||
}
|
||||
val toolsScreenUiState: StateFlow<ToolsScreenUiState> =
|
||||
toolRepository.getAllToolsStream()
|
||||
.map {
|
||||
if (it.isEmpty()) {
|
||||
ToolsScreenUiState.Nothing
|
||||
} else {
|
||||
ToolsScreenUiState.Success(it)
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = ToolsScreenUiState.Loading,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
sealed interface 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 CURRENT_PAGE = "currentPage"
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="app_name">氧工具</string>
|
||||
<string name="app_full_name">氧工具</string>
|
||||
<string name="app_description">All in One</string>
|
||||
|
||||
<string name="core_ok">完成</string>
|
||||
<string name="core_back">返回</string>
|
||||
<string name="core_close">关闭</string>
|
||||
@@ -11,8 +12,26 @@
|
||||
<string name="core_search">搜索</string>
|
||||
<string name="core_loading">加载中…</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_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_settings_title">设置</string>
|
||||
<string name="feature_settings_language">语言</string>
|
||||
<string name="feature_settings_language_system_default">系统默认</string>
|
||||
@@ -34,6 +53,4 @@
|
||||
<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_navigation_icon_description">搜索</string>
|
||||
<string name="feature_tools_description">简介</string>
|
||||
<string name="feature_tools_can_not_open">⚠️ 无法打开工具</string>
|
||||
</resources>
|
||||
@@ -2,6 +2,7 @@
|
||||
<string name="app_name">Oxygen</string>
|
||||
<string name="app_full_name">Oxygen Toolbox</string>
|
||||
<string name="app_description">All in One</string>
|
||||
|
||||
<string name="core_ok">OK</string>
|
||||
<string name="core_back">Back</string>
|
||||
<string name="core_close">Close</string>
|
||||
@@ -10,8 +11,26 @@
|
||||
<string name="core_search">Search</string>
|
||||
<string name="core_loading">Loading…</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_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_settings_title">Settings</string>
|
||||
<string name="feature_settings_language">Language</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_top_app_bar_action_icon_description">More</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>
|
||||
@@ -9,4 +9,6 @@ plugins {
|
||||
alias(libs.plugins.protobuf) apply false
|
||||
alias(libs.plugins.kotlinxSerialization) 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
|
||||
|
||||
# Enable caching between builds.
|
||||
org.gradle.caching=false
|
||||
org.gradle.caching=true
|
||||
|
||||
# Enable configuration caching between builds.
|
||||
org.gradle.configuration-cache=true
|
||||
|
||||
@@ -37,6 +37,7 @@ room = "2.6.1"
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", 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" }
|
||||
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
|
||||
Reference in New Issue
Block a user