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.protobuf)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.secrets)
}
android {
@@ -58,6 +59,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.12"
@@ -113,10 +115,15 @@ afterEvaluate {
tasks.findByName("kspReleaseKotlin")?.dependsOn(tasks.findByName("generateReleaseProto"))
}
secrets {
defaultPropertiesFileName = "secrets.defaults.properties"
}
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
testImplementation(libs.junit)
testImplementation(libs.paging.common)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
@@ -156,4 +163,9 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
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">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
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
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 kotlin.random.Random
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.TimeZoneMonitor
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
import top.fatweb.oxygen.toolbox.repository.lib.LocalDepRepository
import top.fatweb.oxygen.toolbox.repository.tool.LocalToolRepository
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.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
@Module
@@ -28,7 +28,7 @@ abstract class DataModule {
internal abstract fun bindsUserDataRepository(userDataRepository: LocalUserDataRepository): UserDataRepository
@Binds
internal abstract fun bindsToolRepository(toolRepository: LocalToolRepository): ToolRepository
internal abstract fun bindsToolRepository(toolRepository: NetworkToolRepository): ToolRepository
@Binds
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
import androidx.compose.ui.graphics.vector.ImageVector
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
import kotlinx.datetime.LocalDateTime
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.flow.Flow
@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import top.fatweb.oxygen.toolbox.data.lib.DepDataSource
import top.fatweb.oxygen.toolbox.model.lib.Dependencies
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
import javax.inject.Inject
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
import androidx.paging.PagingData
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 {
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 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.ThemeBrandConfig
import top.fatweb.oxygen.toolbox.model.userdata.UserData
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
import javax.inject.Inject
internal class LocalUserDataRepository @Inject constructor(

View File

@@ -1,3 +1,4 @@
/*
package top.fatweb.oxygen.toolbox.ui.component
import androidx.compose.animation.AnimatedVisibility
@@ -202,4 +203,4 @@ fun ToolGroupContentPreview() {
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.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(
toolsScreenUiState: ToolsScreenUiState
toolStorePagingItems: LazyPagingItems<Tool>
) {
when (toolsScreenUiState) {
ToolsScreenUiState.Loading -> Unit
is ToolsScreenUiState.Success -> {
items(
items = toolsScreenUiState.toolGroups,
key = { it.id },
items = toolStorePagingItems.itemSnapshotList,
key = { it!!.id },
) {
ToolGroupCard(toolGroup = it)
}
}
Text(text = it!!.name)
}
}

View File

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

View File

@@ -1,34 +1,44 @@
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.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import top.fatweb.oxygen.toolbox.model.tool.ToolGroup
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 top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class ToolsScreenViewModel @Inject constructor(
toolRepository: ToolRepository
private val toolRepository: ToolRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
val toolsScreenUiState: StateFlow<ToolsScreenUiState> =
toolRepository.toolGroups
.map {
ToolsScreenUiState.Success(it)
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
private val currentPage = savedStateHandle.getStateFlow(CURRENT_PAGE, 1)
@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 {
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.protobuf) 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"
protobufPlugin = "0.9.4"
kotlinxSerialization = "1.9.23"
secrets = "2.0.1"
paging = "3.2.1"
desugarJdkLibs = "2.0.4"
composeBom = "2024.05.00"
@@ -24,6 +26,9 @@ protobuf = "3.25.2"
androidxNavigation = "2.7.7"
androidxHiltNavigationCompose = "1.2.0"
kotlinxSerializationJson = "1.6.3"
retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "1.0.0"
okhttp = "4.12.0"
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
@@ -33,11 +38,13 @@ aboutlibraries = {id = "com.mikepenz.aboutlibraries.plugin", version.ref = "abou
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinxSerialization" }
secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }
[libraries]
desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugarJdkLibs" }
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-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@@ -77,3 +84,8 @@ androidx-navigation-compose = { group = "androidx.navigation", name = "navigatio
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" }
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"