Feat(Tool): Add tool store

Add tool store, support install tool online.
This commit is contained in:
2024-08-08 17:56:45 +08:00
parent c1879dfdc8
commit c9c0debb2b
35 changed files with 1078 additions and 179 deletions

View File

@@ -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)
} }

View File

@@ -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')"
]
}
}

View File

@@ -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 }
}
}
}

View File

@@ -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?>
}

View File

@@ -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
} }

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
)
} }

View File

@@ -10,5 +10,5 @@ data class ToolGroup(
val title: String, val title: String,
val tools: List<Tool> = emptyList() val tools: List<ToolEntity> = emptyList()
) )

View File

@@ -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

View File

@@ -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
)
}
}

View File

@@ -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
) )
} }
} }

View File

@@ -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,

View File

@@ -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)

View File

@@ -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 },

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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>>
}

View File

@@ -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)
} }

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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)
} }

View File

@@ -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,12 +203,14 @@ fun ToolInfo(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
text = "ID: $toolId" text = "ID: $toolId"
) )
Text( toolDesc?.let {
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), Text(
style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.bodySmall,
text = "${stringResource(R.string.feature_tools_description)}: $toolDesc" color = MaterialTheme.colorScheme.outline,
) text = "${stringResource(R.string.feature_tools_description)}: $it"
)
}
} }
} }

View File

@@ -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) },
)
}
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)}
)
}
}

View File

@@ -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,45 +86,61 @@ 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 Column(
) modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
item(span = StaggeredGridItemSpan.FullLine) { verticalArrangement = Arrangement.Center
Spacer(modifier = Modifier.height(8.dp)) ) {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) 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) { toolsPanel(
Column( toolItems = toolsScreenUiState.tools,
modifier = Modifier.fillMaxWidth(), onClickToolCard = onNavigateToToolView
horizontalAlignment = Alignment.CenterHorizontally, )
verticalArrangement = Arrangement.Center
) { item(span = StaggeredGridItemSpan.FullLine) {
val angle by infiniteTransition.animateFloat( Spacer(modifier = Modifier.height(8.dp))
initialValue = 0F, Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
targetValue = 360F, }
animationSpec = infiniteRepeatable( }
animation = tween(800, easing = Ease),
), label = "angle"
)
Icon(
modifier = Modifier
.size(32.dp)
.graphicsLayer { rotationZ = angle },
imageVector = OxygenIcons.Loading,
contentDescription = ""
)
} }
} }
@@ -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
}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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" }