diff --git a/app/build.gradle.kts b/app/build.gradle.kts index aaf8e28..c2d67d0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/app/schemas/top.fatweb.oxygen.toolbox.data.tool.ToolDatabase/1.json b/app/schemas/top.fatweb.oxygen.toolbox.data.tool.ToolDatabase/1.json new file mode 100644 index 0000000..92901bc --- /dev/null +++ b/app/schemas/top.fatweb.oxygen.toolbox.data.tool.ToolDatabase/1.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/ToolDatabase.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/ToolDatabase.kt new file mode 100644 index 0000000..e250e22 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/ToolDatabase.kt @@ -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 } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/dao/ToolDao.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/dao/ToolDao.kt new file mode 100644 index 0000000..8974b93 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/dao/ToolDao.kt @@ -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 + + @Query("SELECT * FROM tools ORDER BY updateTime DESC") + fun selectAllTools(): Flow> + + @Query("SELECT * FROM tools WHERE authorUsername = :username and toolId = :toolId LIMIT 1") + fun selectToolByUsernameAndToolId(username: String, toolId: String): Flow +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataModule.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataModule.kt index 621594a..7d50412 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataModule.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataModule.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DatabaseModule.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DatabaseModule.kt new file mode 100644 index 0000000..cd9723d --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DatabaseModule.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt index ffc293f..9f39cd2 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt @@ -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 diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/Converters.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/Converters.kt new file mode 100644 index 0000000..86c5cae --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/Converters.kt @@ -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 = json.encodeToString(stringList) + + @TypeConverter + fun toStringList(stringList: String): List = json.decodeFromString(stringList) + + @TypeConverter + fun fromLocalDateTime(localDateTime: LocalDateTime): String = localDateTime.toString() + + @TypeConverter + fun toLocalDateTime(string: String): LocalDateTime = LocalDateTime.parse(string) +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/Tool.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/ToolEntity.kt similarity index 51% rename from app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/Tool.kt rename to app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/ToolEntity.kt index 0da968c..8e2ee4e 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/Tool.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/ToolEntity.kt @@ -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 - ) } diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/ToolGroup.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/ToolGroup.kt index 7025cad..16335f0 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/ToolGroup.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/tool/ToolGroup.kt @@ -10,5 +10,5 @@ data class ToolGroup( val title: String, - val tools: List = emptyList() + val tools: List = emptyList() ) \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt index 8f77dcb..71cd8f7 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt @@ -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 diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolStoreNavigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolStoreNavigation.kt new file mode 100644 index 0000000..9a31366 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolStoreNavigation.kt @@ -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 + ) + } +} diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt index b27108e..fd127f7 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt @@ -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 ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/TopLevelDestination.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/TopLevelDestination.kt index 3fbdf90..4984405 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/TopLevelDestination.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/TopLevelDestination.kt @@ -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, diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolBaseVo.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolBaseVo.kt index 6796cfb..07d7780 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolBaseVo.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolBaseVo.kt @@ -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) diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolVo.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolVo.kt index d850e1a..17db05e 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolVo.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolVo.kt @@ -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 }, diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/UserWithInfoVo.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/UserWithInfoVo.kt index c2368e3..0a7d68a 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/UserWithInfoVo.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/UserWithInfoVo.kt @@ -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( @@ -19,10 +18,4 @@ data class UserWithInfoVo( val avatar: String ) -} - -fun UserWithInfoVo.asExternalModel() = Tool.Author( - username = username, - nickname = userInfo.nickname, - avatar = userInfo.avatar -) +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/paging/ToolStorePagingSource.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/paging/ToolStorePagingSource.kt index 20dbec8..0e5ae6f 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/paging/ToolStorePagingSource.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/paging/ToolStorePagingSource.kt @@ -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() { - override fun getRefreshKey(state: PagingState): Int? = null +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null - override suspend fun load(params: LoadParams): LoadResult { + override suspend fun load(params: LoadParams): LoadResult { return try { val currentPage = params.key ?: 1 val (_, success, msg, data) = oxygenNetworkDataSource.getStore(searchValue, currentPage) diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/StoreRepository.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/StoreRepository.kt new file mode 100644 index 0000000..374881f --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/StoreRepository.kt @@ -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 + + suspend fun getStore(searchValue: String, currentPage: Int): Flow> + + fun detail( + username: String, + toolId: String, + ver: String = "latest", + platform: ToolEntity.Platform = ToolEntity.Platform.ANDROID + ): Flow> +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/ToolRepository.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/ToolRepository.kt index 5063b3d..5a19362 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/ToolRepository.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/ToolRepository.kt @@ -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 + fun getAllToolsStream(): Flow> - suspend fun getStore(searchValue: String, currentPage: Int): Flow> + fun getToolById(id: Long): Flow - fun detail( - username: String, - toolId: String, - ver: String = "latest", - platform: Tool.Platform = Tool.Platform.ANDROID - ): Flow> + fun getToolByUsernameAndToolId(username: String, toolId: String): Flow + + suspend fun saveTool(toolEntity: ToolEntity) + + suspend fun updateTool(toolEntity: ToolEntity) + + suspend fun removeTool(toolEntity: ToolEntity) } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkToolRepository.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkStoreRepository.kt similarity index 83% rename from app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkToolRepository.kt rename to app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkStoreRepository.kt index a5cd2d0..4d356c9 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkToolRepository.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkStoreRepository.kt @@ -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 get() = toolDataSource.toolViewTemplate - override suspend fun getStore(searchValue: String, currentPage: Int): Flow> = + override suspend fun getStore(searchValue: String, currentPage: Int): Flow> = 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> = + platform: ToolEntity.Platform + ): Flow> = oxygenNetworkDataSource.detail( username, toolId, diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/OfflineToolRepository.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/OfflineToolRepository.kt new file mode 100644 index 0000000..36d1829 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/OfflineToolRepository.kt @@ -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> = + toolDao.selectAllTools() + + override fun getToolById(id: Long): Flow = + toolDao.selectToolById(id) + + override fun getToolByUsernameAndToolId(username: String, toolId: String): Flow = + 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) +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenAppState.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenAppState.kt index 9fb564f..1548e57 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenAppState.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenAppState.kt @@ -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) } diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt index a6b603d..0ae9a9e 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt @@ -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" + ) + } } } diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolStoreScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolStoreScreen.kt new file mode 100644 index 0000000..94caf7e --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolStoreScreen.kt @@ -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, + 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, + 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) }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolStoreViewModel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolStoreViewModel.kt new file mode 100644 index 0000000..4925d6d --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolStoreViewModel.kt @@ -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> = 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" diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreenViewModel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreenViewModel.kt index c16a5e7..deb4387 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreenViewModel.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreenViewModel.kt @@ -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( 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 { - 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) diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsPanel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsPanel.kt deleted file mode 100644 index 7a2ceee..0000000 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsPanel.kt +++ /dev/null @@ -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, - onClickToolCard: (username: String, toolId: String) -> Unit -) { - items( - items = toolStorePagingItems.itemSnapshotList, - key = { it!!.id }, - ) { - ToolCard( - tool = it!!, - onClickToolCard = {onClickToolCard(it.author.username, it.toolId)} - ) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt index ab28215..98fea97 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt @@ -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 + 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)) + } + } } } @@ -136,4 +154,27 @@ internal fun ToolsScreen( onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable) ) } -} \ No newline at end of file +} + +private fun LazyStaggeredGridScope.toolsPanel( + toolItems: List, + 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 + } diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreenViewModel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreenViewModel.kt index 2fac814..aadd859 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreenViewModel.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreenViewModel.kt @@ -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> = combine( - searchValue, - currentPage, - ::Pair - ).flatMapLatest { (searchValue, currentPage) -> - toolRepository.getStore(searchValue, currentPage).cachedIn(viewModelScope) - } + val toolsScreenUiState: StateFlow = + 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) : ToolsScreenUiState + data object Nothing : ToolsScreenUiState + data class Success(val tools: List) : ToolsScreenUiState } private const val SEARCH_VALUE = "searchValue" -private const val CURRENT_PAGE = "currentPage" \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 5f26bca..5482fb2 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -3,6 +3,7 @@ 氧工具 氧工具 All in One + 完成 返回 关闭 @@ -11,8 +12,26 @@ 搜索 加载中… ⚠️ 无法连接至互联网 + 安装 + 安装中…… + 取消 + + 商店 + 安装工具 + 确定安装由用户 %1$s 提供的工具 %2$s 吗? + 安装成功 + 恭喜!工具安装成功。 + 安装失败 + 安装失败!请稍后重试…… + 工具 + 简介 + ⚠️ 无法打开工具 + 暂无工具已安装 + 前往商店… + 收藏 + 设置 语言 系统默认 @@ -34,6 +53,4 @@ 关于 更多 搜索 - 简介 - ⚠️ 无法打开工具 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 138afc2..dbafa95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ Oxygen Oxygen Toolbox All in One + OK Back Close @@ -10,8 +11,26 @@ Search Loading… ⚠️ Unable to connect to the internet + Install + Installing… + Cancel + + Store + Install Tool + Are you sure to install tool %1$s provided by user %2$s? + Install Success + Congratulations, the tool installation is successful. + Install Failed + Installation failed, please try again later… + Tools + Desc + ⚠️ Can not open the tool + No tools installed yet + Go to store… + Star + Settings Language System Default @@ -35,6 +54,4 @@ About More Search - Desc - ⚠️ Can not open the tool \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index fce884e..68001f8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 101c2fe..55c4bc9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 946b9e9..b9c18bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }