Feat(ToolScreen): Support get tool online
Support get tool online in page
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>>
|
||||
}
|
||||
@@ -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("")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
13
app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/Page.kt
Normal file
13
app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/Page.kt
Normal 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>
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>>
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
4
secrets.defaults.properties
Normal file
4
secrets.defaults.properties
Normal 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"
|
||||
Reference in New Issue
Block a user