Feat(ToolScreen): Support get tool online

Support get tool online in page
This commit is contained in:
2024-05-10 05:47:30 +08:00
parent b2cbea5383
commit c596767c37
31 changed files with 710 additions and 149 deletions

View File

@@ -9,6 +9,7 @@ plugins {
alias(libs.plugins.hilt) alias(libs.plugins.hilt)
alias(libs.plugins.protobuf) alias(libs.plugins.protobuf)
alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.secrets)
} }
android { android {
@@ -58,6 +59,7 @@ android {
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.5.12" kotlinCompilerExtensionVersion = "1.5.12"
@@ -113,10 +115,15 @@ afterEvaluate {
tasks.findByName("kspReleaseKotlin")?.dependsOn(tasks.findByName("generateReleaseProto")) tasks.findByName("kspReleaseKotlin")?.dependsOn(tasks.findByName("generateReleaseProto"))
} }
secrets {
defaultPropertiesFileName = "secrets.defaults.properties"
}
dependencies { dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.paging.common)
androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
@@ -156,4 +163,9 @@ dependencies {
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.retrofit.core)
implementation(libs.retrofit.kotlin.serialization)
implementation(libs.okhttp.logging)
implementation(libs.paging.runtime)
implementation(libs.paging.compose)
} }

View File

@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".OxygenApplication" android:name=".OxygenApplication"

View File

@@ -0,0 +1,22 @@
package top.fatweb.oxygen.toolbox.data.network
import kotlinx.coroutines.flow.Flow
import top.fatweb.oxygen.toolbox.model.Result
import top.fatweb.oxygen.toolbox.network.model.PageVo
import top.fatweb.oxygen.toolbox.network.model.ResponseResult
import top.fatweb.oxygen.toolbox.network.model.ToolBaseVo
import top.fatweb.oxygen.toolbox.network.model.ToolVo
interface OxygenNetworkDataSource {
suspend fun getStore(
searchValue: String = "",
currentPage: Int = 1
): ResponseResult<PageVo<ToolVo>>
suspend fun detail(
username: String,
toolId: String,
ver: String = "latest",
platform: ToolBaseVo.Platform = ToolBaseVo.Platform.ANDROID
): Flow<Result<ToolVo>>
}

View File

@@ -1,36 +1,7 @@
package top.fatweb.oxygen.toolbox.data.tool package top.fatweb.oxygen.toolbox.data.tool
import kotlinx.coroutines.flow.flowOf
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.model.tool.Tool
import top.fatweb.oxygen.toolbox.model.tool.ToolGroup
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random
class ToolDataSource @Inject constructor() { class ToolDataSource @Inject constructor() {
val tool = flowOf(
(0..100).map { index ->
ToolGroup(
id = "local-base-$index",
title = "${generateRandomString()}-$index",
icon = OxygenIcons.Tool,
tools = (0..20).map {
Tool(
id = "local-base-$index-time-screen-$it",
icon = OxygenIcons.Time,
name = "${generateRandomString()}-$index-$it"
)
}
)
}
)
private fun generateRandomString(length: Int = (1..10).random()): String {
val words = ('a'..'z') + ('A'..'Z')
return (1..length)
.map { Random.nextInt(0, words.size) }
.map(words::get)
.joinToString("")
}
} }

View File

@@ -9,10 +9,10 @@ import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
import top.fatweb.oxygen.toolbox.monitor.TimeZoneBroadcastMonitor 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.LocalDepRepository import top.fatweb.oxygen.toolbox.repository.lib.impl.LocalDepRepository
import top.fatweb.oxygen.toolbox.repository.tool.LocalToolRepository import top.fatweb.oxygen.toolbox.repository.tool.impl.NetworkToolRepository
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
import top.fatweb.oxygen.toolbox.repository.userdata.LocalUserDataRepository import top.fatweb.oxygen.toolbox.repository.userdata.impl.LocalUserDataRepository
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
@Module @Module
@@ -28,7 +28,7 @@ abstract class DataModule {
internal abstract fun bindsUserDataRepository(userDataRepository: LocalUserDataRepository): UserDataRepository internal abstract fun bindsUserDataRepository(userDataRepository: LocalUserDataRepository): UserDataRepository
@Binds @Binds
internal abstract fun bindsToolRepository(toolRepository: LocalToolRepository): ToolRepository internal abstract fun bindsToolRepository(toolRepository: NetworkToolRepository): ToolRepository
@Binds @Binds
internal abstract fun bindsDepRepository(depRepository: LocalDepRepository): DepRepository internal abstract fun bindsDepRepository(depRepository: LocalDepRepository): DepRepository

View File

@@ -0,0 +1,45 @@
package top.fatweb.oxygen.toolbox.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import top.fatweb.oxygen.toolbox.BuildConfig
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
import top.fatweb.oxygen.toolbox.network.retrofit.RetrofitOxygenNetwork
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
@Provides
@Singleton
fun providesNetworkJson(): Json = Json {
ignoreUnknownKeys = true
}
@Provides
@Singleton
fun okHttpCallFactory(): Call.Factory =
OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor()
.apply {
if (BuildConfig.DEBUG) {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
}
)
.build()
@Provides
@Singleton
fun providesOxygenNetworkDataSource(
networkJson: Json,
okhttpCallFactory: dagger.Lazy<Call.Factory>
): OxygenNetworkDataSource = RetrofitOxygenNetwork(networkJson, okhttpCallFactory)
}

View File

@@ -0,0 +1,13 @@
package top.fatweb.oxygen.toolbox.model
data class Page<T>(
val total: Long,
val pages: Long,
val size: Long,
val current: Long,
val records: List<T>
)

View File

@@ -0,0 +1,41 @@
package top.fatweb.oxygen.toolbox.model
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import top.fatweb.oxygen.toolbox.network.model.ResponseResult
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Fail(val message: String): Result<Nothing>
data class Error(val exception: Throwable) : Result<Nothing>
data object Loading : Result<Nothing>
}
fun <T> Flow<ResponseResult<T>>.asResult(): Flow<Result<T>> = map<ResponseResult<T>, Result<T>> {
if (it.success) {
Result.Success(it.data!!)
} else {
Result.Fail(it.msg)
}
}
.onStart { emit(Result.Loading) }
.catch { emit(Result.Error(it)) }
fun <T, R> Result<T>.asExternalModel(block: (T) -> R): Result<R> =
when (this) {
is Result.Success -> {
Result.Success(block(data))
}
is Result.Fail -> {
Result.Fail(message)
}
is Result.Error -> {
Result.Error(exception)
}
Result.Loading -> Result.Loading
}

View File

@@ -1,12 +1,53 @@
package top.fatweb.oxygen.toolbox.model.tool package top.fatweb.oxygen.toolbox.model.tool
import androidx.compose.ui.graphics.vector.ImageVector import kotlinx.datetime.LocalDateTime
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
data class Tool( data class Tool(
val id: String, val id: Long,
val icon: ImageVector = OxygenIcons.Tool, val name: String,
val name: String val toolId: String,
)
val icon: String,
val platform: Platform,
val description: String,
val base: String? = null,
val author: Author,
val ver: String,
val keywords: List<String>,
val categories: List<String>,
val source: String? = null,
val dist: String? = null,
val entryPoint: String,
val createTime: LocalDateTime,
val updateTime: LocalDateTime
) {
enum class Platform {
WEB,
DESKTOP,
ANDROID
}
data class Author(
val username: String,
val nickname: String,
val avatar: String
)
}

View File

@@ -0,0 +1,25 @@
package top.fatweb.oxygen.toolbox.network.model
import kotlinx.serialization.Serializable
import top.fatweb.oxygen.toolbox.model.Page
@Serializable
data class PageVo<T>(
val total: Long,
val pages: Long,
val size: Long,
val current: Long,
val records: List<T>
)
fun <T, R> PageVo<T>.asExternalModel(block: (T) -> R): Page<R> = Page(
total = total,
pages = pages,
size = size,
current = current,
records = records.map(block)
)

View File

@@ -0,0 +1,14 @@
package top.fatweb.oxygen.toolbox.network.model
import kotlinx.serialization.Serializable
@Serializable
data class ResponseResult<T>(
val code: Long,
val success: Boolean,
val msg: String,
val data: T? = null
)

View File

@@ -0,0 +1,42 @@
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.network.serializer.LocalDateTimeSerializer
@Serializable
data class ToolBaseVo(
val id: Long,
val name: String,
val source: ToolDataVo? = null,
val dist: ToolDataVo,
val platform: Platform? = null,
val compiled: Boolean? = null,
@Serializable(LocalDateTimeSerializer::class)
val createTime: LocalDateTime? = null,
@Serializable(LocalDateTimeSerializer::class)
val updateTime: LocalDateTime? = null
) {
@Serializable
enum class Platform {
@SerialName("WEB")
WEB,
@SerialName("DESKTOP")
DESKTOP,
@SerialName("ANDROID")
ANDROID
}
}
fun ToolBaseVo.Platform.asExternalModel() = Tool.Platform.valueOf(this.name)

View File

@@ -0,0 +1,20 @@
package top.fatweb.oxygen.toolbox.network.model
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
@Serializable
data class ToolCategoryVo(
val id: Long,
val name: String,
val enable: Boolean,
@Serializable(LocalDateTimeSerializer::class)
val createTime: LocalDateTime,
@Serializable(LocalDateTimeSerializer::class)
val updateTime: LocalDateTime
)

View File

@@ -0,0 +1,18 @@
package top.fatweb.oxygen.toolbox.network.model
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
@Serializable
data class ToolDataVo(
val id: Long,
val data: String,
@Serializable(LocalDateTimeSerializer::class)
val createTime: LocalDateTime,
@Serializable(LocalDateTimeSerializer::class)
val updateTime: LocalDateTime
)

View File

@@ -0,0 +1,82 @@
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.network.serializer.LocalDateTimeSerializer
@Serializable
data class ToolVo(
val id: Long,
val name: String,
val toolId: String,
val icon: String,
val platform: ToolBaseVo.Platform,
val description: String,
val base: ToolBaseVo? = null,
val author: UserWithInfoVo,
val ver: String,
val keywords: List<String>,
val categories: List<ToolCategoryVo>,
val source: ToolDataVo? = null,
val dist: ToolDataVo? = null,
val entryPoint: String,
val publish: Long,
val review: ReviewType,
@Serializable(LocalDateTimeSerializer::class)
val createTime: LocalDateTime,
@Serializable(LocalDateTimeSerializer::class)
val updateTime: LocalDateTime
) {
@Serializable
enum class ReviewType {
@SerialName("NONE")
NONE,
@SerialName("PROCESSING")
PROCESSING,
@SerialName("PASS")
PASS,
@SerialName("REJECT")
REJECT
}
}
fun ToolVo.asExternalModel() = Tool(
id = id,
name = name,
toolId = toolId,
icon = icon,
platform = platform.asExternalModel(),
description = description,
base = base?.dist?.data,
author = author.asExternalModel(),
ver = ver,
keywords = keywords,
categories = categories.map { it.name },
source = source?.data,
dist = dist?.data,
entryPoint = entryPoint,
createTime = createTime,
updateTime = updateTime
)

View File

@@ -0,0 +1,28 @@
package top.fatweb.oxygen.toolbox.network.model
import kotlinx.serialization.Serializable
import top.fatweb.oxygen.toolbox.model.tool.Tool
@Serializable
data class UserWithInfoVo(
val id: Long,
val username: String,
val userInfo: UserInfoVo
) {
@Serializable
data class UserInfoVo(
val id: Long,
val nickname: String,
val avatar: String
)
}
fun UserWithInfoVo.asExternalModel() = Tool.Author(
username = username,
nickname = userInfo.nickname,
avatar = userInfo.avatar
)

View File

@@ -0,0 +1,37 @@
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.network.model.ToolVo
import top.fatweb.oxygen.toolbox.network.model.asExternalModel
internal class ToolStorePagingSource(
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
private val searchValue: String
) : PagingSource<Int, Tool>() {
override fun getRefreshKey(state: PagingState<Int, Tool>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Tool> {
return try {
val currentPage = params.key ?: 1
val (_, success, msg, data) = oxygenNetworkDataSource.getStore(searchValue, currentPage)
if (!success) {
return LoadResult.Error(RuntimeException(msg))
}
val (_, pages, _, _, records) = data!!
val nextPage = if (currentPage < pages) currentPage + 1 else null
LoadResult.Page(
data = records.map(ToolVo::asExternalModel),
prevKey = null,
nextKey = nextPage
)
} catch (e: Throwable) {
LoadResult.Error(e)
}
}
}

View File

@@ -0,0 +1,76 @@
package top.fatweb.oxygen.toolbox.network.retrofit
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
import top.fatweb.oxygen.toolbox.BuildConfig
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
import top.fatweb.oxygen.toolbox.model.Result
import top.fatweb.oxygen.toolbox.model.asResult
import top.fatweb.oxygen.toolbox.network.model.PageVo
import top.fatweb.oxygen.toolbox.network.model.ResponseResult
import top.fatweb.oxygen.toolbox.network.model.ToolBaseVo
import top.fatweb.oxygen.toolbox.network.model.ToolVo
import javax.inject.Inject
private interface RetrofitOxygenNetworkApi {
@GET(value = "/tool/store")
suspend fun getStore(
@Query("currentPage") currentPage: Int,
@Query("searchValue") searchValue: String,
@Query("platform") platform: ToolBaseVo.Platform? = ToolBaseVo.Platform.ANDROID
): ResponseResult<PageVo<ToolVo>>
@GET(value = "/tool/detail/{username}/{toolId}/{ver}")
suspend fun detail(
@Path("username") username: String,
@Path("toolId") toolId: String,
@Path("ver") ver: String,
@Query("platform") platform: String
): ResponseResult<ToolVo>
}
private const val API_BASE_URL = BuildConfig.API_URL
internal class RetrofitOxygenNetwork @Inject constructor(
networkJson: Json,
okhttpCallFactory: dagger.Lazy<Call.Factory>
) : OxygenNetworkDataSource {
private val networkApi = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.callFactory { okhttpCallFactory.get().newCall(it) }
.addConverterFactory(
networkJson.asConverterFactory("application/json".toMediaType())
)
.build()
.create(RetrofitOxygenNetworkApi::class.java)
override suspend fun getStore(
searchValue: String,
currentPage: Int
): ResponseResult<PageVo<ToolVo>> =
networkApi.getStore(searchValue = searchValue, currentPage = currentPage)
override suspend fun detail(
username: String,
toolId: String,
ver: String,
platform: ToolBaseVo.Platform
): Flow<Result<ToolVo>> = flow {
emit(
networkApi.detail(
username = username,
toolId = toolId,
ver = ver,
platform = platform.name
)
)
}.asResult()
}

View File

@@ -0,0 +1,23 @@
package top.fatweb.oxygen.toolbox.network.serializer
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): LocalDateTime =
LocalDateTime.parse(decoder.decodeString().removeSuffix("Z"))
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.toString().padEnd(24, 'Z'))
}
}

View File

@@ -1,4 +1,4 @@
package top.fatweb.oxygen.toolbox.repository.lib package top.fatweb.oxygen.toolbox.repository.lib.impl
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import top.fatweb.oxygen.toolbox.data.lib.DepDataSource import top.fatweb.oxygen.toolbox.data.lib.DepDataSource
import top.fatweb.oxygen.toolbox.model.lib.Dependencies import top.fatweb.oxygen.toolbox.model.lib.Dependencies
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
import javax.inject.Inject import javax.inject.Inject
class LocalDepRepository @Inject constructor( class LocalDepRepository @Inject constructor(

View File

@@ -1,13 +0,0 @@
package top.fatweb.oxygen.toolbox.repository.tool
import kotlinx.coroutines.flow.Flow
import top.fatweb.oxygen.toolbox.data.tool.ToolDataSource
import top.fatweb.oxygen.toolbox.model.tool.ToolGroup
import javax.inject.Inject
internal class LocalToolRepository @Inject constructor(
toolDataSource: ToolDataSource
) : ToolRepository {
override val toolGroups: Flow<List<ToolGroup>> =
toolDataSource.tool
}

View File

@@ -1,8 +1,17 @@
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.tool.ToolGroup import top.fatweb.oxygen.toolbox.model.Result
import top.fatweb.oxygen.toolbox.model.tool.Tool
interface ToolRepository { interface ToolRepository {
val toolGroups: Flow<List<ToolGroup>> suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<Tool>>
suspend fun detail(
username: String,
toolId: String,
ver: String = "latest",
platform: Tool.Platform = Tool.Platform.ANDROID
): Flow<Result<Tool>>
} }

View File

@@ -0,0 +1,45 @@
package top.fatweb.oxygen.toolbox.repository.tool.impl
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
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.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 javax.inject.Inject
private const val PAGE_SIZE = 20
internal class NetworkToolRepository @Inject constructor(
private val oxygenNetworkDataSource: OxygenNetworkDataSource
) : ToolRepository {
override suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<Tool>> =
Pager(
config = PagingConfig(PAGE_SIZE),
pagingSourceFactory = { ToolStorePagingSource(oxygenNetworkDataSource, searchValue) }
).flow
override suspend fun detail(
username: String,
toolId: String,
ver: String,
platform: Tool.Platform
): Flow<Result<Tool>> =
oxygenNetworkDataSource.detail(
username,
toolId,
ver,
ToolBaseVo.Platform.valueOf(platform.name)
).map {
it.asExternalModel(ToolVo::asExternalModel)
}
}

View File

@@ -1,4 +1,4 @@
package top.fatweb.oxygen.toolbox.repository.userdata package top.fatweb.oxygen.toolbox.repository.userdata.impl
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import top.fatweb.oxygen.toolbox.data.userdata.OxygenPreferencesDataSource import top.fatweb.oxygen.toolbox.data.userdata.OxygenPreferencesDataSource
@@ -7,6 +7,7 @@ import top.fatweb.oxygen.toolbox.model.userdata.LanguageConfig
import top.fatweb.oxygen.toolbox.model.userdata.LaunchPageConfig import top.fatweb.oxygen.toolbox.model.userdata.LaunchPageConfig
import top.fatweb.oxygen.toolbox.model.userdata.ThemeBrandConfig import top.fatweb.oxygen.toolbox.model.userdata.ThemeBrandConfig
import top.fatweb.oxygen.toolbox.model.userdata.UserData import top.fatweb.oxygen.toolbox.model.userdata.UserData
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
import javax.inject.Inject import javax.inject.Inject
internal class LocalUserDataRepository @Inject constructor( internal class LocalUserDataRepository @Inject constructor(

View File

@@ -1,3 +1,4 @@
/*
package top.fatweb.oxygen.toolbox.ui.component package top.fatweb.oxygen.toolbox.ui.component
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@@ -202,4 +203,4 @@ fun ToolGroupContentPreview() {
ToolDataSource().tool.first().map { it.tools }.flatten() ToolDataSource().tool.first().map { it.tools }.flatten()
}) })
} }
} }*/

View File

@@ -2,21 +2,17 @@ package top.fatweb.oxygen.toolbox.ui.tool
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.items
import top.fatweb.oxygen.toolbox.ui.component.ToolGroupCard import androidx.compose.material3.Text
import androidx.paging.compose.LazyPagingItems
import top.fatweb.oxygen.toolbox.model.tool.Tool
fun LazyStaggeredGridScope.toolsPanel( fun LazyStaggeredGridScope.toolsPanel(
toolsScreenUiState: ToolsScreenUiState toolStorePagingItems: LazyPagingItems<Tool>
) { ) {
when (toolsScreenUiState) {
ToolsScreenUiState.Loading -> Unit
is ToolsScreenUiState.Success -> {
items( items(
items = toolsScreenUiState.toolGroups, items = toolStorePagingItems.itemSnapshotList,
key = { it.id }, key = { it!!.id },
) { ) {
ToolGroupCard(toolGroup = it) Text(text = it!!.name)
}
}
} }
} }

View File

@@ -1,5 +1,6 @@
package top.fatweb.oxygen.toolbox.ui.tool package top.fatweb.oxygen.toolbox.ui.tool
import android.util.Log
import androidx.activity.compose.ReportDrawnWhen import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -19,48 +20,44 @@ 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.rememberLazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.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.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState
import kotlinx.coroutines.flow.first import androidx.paging.compose.LazyPagingItems
import kotlinx.coroutines.runBlocking import androidx.paging.compose.collectAsLazyPagingItems
import top.fatweb.oxygen.toolbox.R import top.fatweb.oxygen.toolbox.model.tool.Tool
import top.fatweb.oxygen.toolbox.data.tool.ToolDataSource
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
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
@Composable @Composable
internal fun ToolsRoute( internal fun ToolsRoute(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ToolsScreenViewModel = hiltViewModel() viewModel: ToolsScreenViewModel = hiltViewModel()
) { ) {
val toolsScreenUiState by viewModel.toolsScreenUiState.collectAsStateWithLifecycle() val toolStorePagingItems = viewModel.getStoreData().collectAsLazyPagingItems()
ToolsScreen( ToolsScreen(
modifier = modifier, modifier = modifier,
toolsScreenUiState = toolsScreenUiState toolStorePagingItems = toolStorePagingItems
) )
} }
@Composable @Composable
internal fun ToolsScreen( internal fun ToolsScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
toolsScreenUiState: ToolsScreenUiState toolStorePagingItems: LazyPagingItems<Tool>
) { ) {
val isToolLoading = toolsScreenUiState is ToolsScreenUiState.Loading val isToolLoading = toolStorePagingItems.loadState.refresh is LoadState.Loading
Log.d("TAG", "ToolsScreen: ${toolStorePagingItems.loadState}")
ReportDrawnWhen { !isToolLoading } ReportDrawnWhen { !isToolLoading }
val itemsAvailable = howManyItems(toolsScreenUiState) val itemsAvailable = toolStorePagingItems.itemCount
val state = rememberLazyStaggeredGridState() val state = rememberLazyStaggeredGridState()
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable) val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
@@ -68,12 +65,6 @@ internal fun ToolsScreen(
Box( Box(
modifier.fillMaxSize() modifier.fillMaxSize()
) { ) {
when (toolsScreenUiState) {
ToolsScreenUiState.Loading -> {
Text(text = stringResource(R.string.feature_settings_loading))
}
is ToolsScreenUiState.Success -> {
LazyVerticalStaggeredGrid( LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(300.dp), columns = StaggeredGridCells.Adaptive(300.dp),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
@@ -82,7 +73,7 @@ internal fun ToolsScreen(
state = state state = state
) { ) {
toolsPanel(toolsScreenUiState = toolsScreenUiState) toolsPanel(toolStorePagingItems = toolStorePagingItems)
item(span = StaggeredGridItemSpan.FullLine) { item(span = StaggeredGridItemSpan.FullLine) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -100,17 +91,9 @@ internal fun ToolsScreen(
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable) onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
) )
} }
}
}
} }
fun howManyItems(toolScreenUiState: ToolsScreenUiState) = /*
when (toolScreenUiState) {
ToolsScreenUiState.Loading -> 0
is ToolsScreenUiState.Success -> toolScreenUiState.toolGroups.size
}
@OxygenPreviews @OxygenPreviews
@Composable @Composable
fun ToolsScreenLoadingPreview() { fun ToolsScreenLoadingPreview() {
@@ -130,4 +113,4 @@ fun ToolsScreenPreview() {
}) })
) )
} }
} }*/

View File

@@ -1,34 +1,44 @@
package top.fatweb.oxygen.toolbox.ui.tool package top.fatweb.oxygen.toolbox.ui.tool
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.flow.SharingStarted import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.flatMapLatest
import top.fatweb.oxygen.toolbox.model.tool.ToolGroup import top.fatweb.oxygen.toolbox.model.Page
import top.fatweb.oxygen.toolbox.model.tool.Tool
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(
toolRepository: ToolRepository private val toolRepository: ToolRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() { ) : ViewModel() {
val toolsScreenUiState: StateFlow<ToolsScreenUiState> = private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
toolRepository.toolGroups private val currentPage = savedStateHandle.getStateFlow(CURRENT_PAGE, 1)
.map {
ToolsScreenUiState.Success(it) @OptIn(ExperimentalCoroutinesApi::class)
fun getStoreData(): Flow<PagingData<Tool>> {
return combine(
searchValue,
currentPage,
::Pair
).flatMapLatest { (searchValue, currentPage) ->
toolRepository.getStore(searchValue, currentPage).cachedIn(viewModelScope)
}
} }
.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 toolGroups: List<ToolGroup>) : ToolsScreenUiState data class Success(val tools: Page<Tool>) : ToolsScreenUiState
} }
private const val SEARCH_VALUE = "searchValue"
private const val CURRENT_PAGE = "currentPage"

View File

@@ -7,4 +7,5 @@ plugins {
alias(libs.plugins.hilt) apply false alias(libs.plugins.hilt) apply false
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
} }

View File

@@ -5,6 +5,8 @@ ksp = "1.9.23-1.0.20"
aboutlibraries = "11.1.0" aboutlibraries = "11.1.0"
protobufPlugin = "0.9.4" protobufPlugin = "0.9.4"
kotlinxSerialization = "1.9.23" kotlinxSerialization = "1.9.23"
secrets = "2.0.1"
paging = "3.2.1"
desugarJdkLibs = "2.0.4" desugarJdkLibs = "2.0.4"
composeBom = "2024.05.00" composeBom = "2024.05.00"
@@ -24,20 +26,25 @@ protobuf = "3.25.2"
androidxNavigation = "2.7.7" androidxNavigation = "2.7.7"
androidxHiltNavigationCompose = "1.2.0" androidxHiltNavigationCompose = "1.2.0"
kotlinxSerializationJson = "1.6.3" kotlinxSerializationJson = "1.6.3"
retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "1.0.0"
okhttp = "4.12.0"
[plugins] [plugins]
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" }
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" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinxSerialization" } kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinxSerialization" }
secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }
[libraries] [libraries]
desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugarJdkLibs"} desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugarJdkLibs" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
paging-common = { group = "androidx.paging", name = "paging-common", version.ref = "paging" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@@ -51,19 +58,19 @@ androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
material-icons-core = { group = "androidx.compose.material", name = "material-icons-core"} material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" }
material-icons-extended = {group = "androidx.compose.material", name = "material-icons-extended"} material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
material3-window-size = {group = "androidx.compose.material3", name = "material3-window-size-class"} material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
lifecycle-runtime-testing = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" } lifecycle-runtime-testing = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" }
lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle"} lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" }
dagger-compiler = { group = "com.google.dagger", name = "dagger-compiler", version.ref = "hilt" } dagger-compiler = { group = "com.google.dagger", name = "dagger-compiler", version.ref = "hilt" }
hilt-android = {group = "com.google.dagger", name = "hilt-android", version.ref = "hilt"} hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" } coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
@@ -76,4 +83,9 @@ protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" } androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" }
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }
kotlinx-serialization-json = {group="org.jetbrains.kotlinx", name="kotlinx-serialization-json", version.ref = "kotlinxSerializationJson"} kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerializationJson" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" }
paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }

View File

@@ -0,0 +1,4 @@
## This file provides default values to modules using the secrets-gradle-plugin. It is necessary
# because the secrets properties file is not under source control so CI builds will fail without
# default values.
API_URL="https://example.com"