Feat(ToolView): Add ToolView to support execute tool
This commit is contained in:
@@ -169,4 +169,5 @@ dependencies {
|
|||||||
implementation(libs.paging.runtime)
|
implementation(libs.paging.runtime)
|
||||||
implementation(libs.paging.compose)
|
implementation(libs.paging.compose)
|
||||||
implementation(libs.androidsvg.aar)
|
implementation(libs.androidsvg.aar)
|
||||||
|
implementation(libs.compose.webview)
|
||||||
}
|
}
|
||||||
18
app/src/main/assets/template/tool-view.html
Normal file
18
app/src/main/assets/template/tool-view.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Preview</title>
|
||||||
|
<!-- es-module-shims -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" id="appSrc">{{replace_code}}</script>
|
||||||
|
<div id="root">
|
||||||
|
<div
|
||||||
|
style="position:absolute;top: 0;left:0;width:100%;height:100%;display: flex;justify-content: center;align-items: center;">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -13,7 +13,7 @@ interface OxygenNetworkDataSource {
|
|||||||
currentPage: Int = 1
|
currentPage: Int = 1
|
||||||
): ResponseResult<PageVo<ToolVo>>
|
): ResponseResult<PageVo<ToolVo>>
|
||||||
|
|
||||||
suspend fun detail(
|
fun detail(
|
||||||
username: String,
|
username: String,
|
||||||
toolId: String,
|
toolId: String,
|
||||||
ver: String = "latest",
|
ver: String = "latest",
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
package top.fatweb.oxygen.toolbox.data.tool
|
package top.fatweb.oxygen.toolbox.data.tool
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||||
|
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ToolDataSource @Inject constructor() {
|
class ToolDataSource @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
|
@Dispatcher(OxygenDispatchers.IO) private val ioDispatcher: CoroutineDispatcher
|
||||||
|
) {
|
||||||
|
val toolViewTemplate = flow {
|
||||||
|
emit(
|
||||||
|
context.assets.open("template/tool-view.html")
|
||||||
|
.bufferedReader()
|
||||||
|
.use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.flowOn(ioDispatcher)
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,8 @@ import androidx.compose.ui.graphics.ImageBitmap
|
|||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import com.caverock.androidsvg.SVG
|
import com.caverock.androidsvg.SVG
|
||||||
|
import top.fatweb.oxygen.toolbox.util.decodeToByteArray
|
||||||
|
import top.fatweb.oxygen.toolbox.util.decodeToString
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@@ -42,22 +44,16 @@ object OxygenIcons {
|
|||||||
val Time = Icons.Default.AccessTime
|
val Time = Icons.Default.AccessTime
|
||||||
val Tool = Icons.Default.Build
|
val Tool = Icons.Default.Build
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
fun fromSvgBase64(base64String: String): ImageBitmap {
|
fun fromSvgBase64(base64String: String): ImageBitmap {
|
||||||
val svg = SVG.getFromString(base64DecodeToString(base64String))
|
val svg = SVG.getFromString(Base64.decodeToString(base64String))
|
||||||
val drawable = PictureDrawable(svg.renderToPicture())
|
val drawable = PictureDrawable(svg.renderToPicture())
|
||||||
return drawable.toBitmap().asImageBitmap()
|
return drawable.toBitmap().asImageBitmap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
fun fromPngBase64(base64String: String): ImageBitmap {
|
fun fromPngBase64(base64String: String): ImageBitmap {
|
||||||
val byteArray = base64DecodeToByteArray(base64String)
|
val byteArray = Base64.decodeToByteArray(base64String)
|
||||||
return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size).asImageBitmap()
|
return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size).asImageBitmap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalEncodingApi::class)
|
|
||||||
private fun base64DecodeToString(base64String: String): String =
|
|
||||||
Base64.decode(base64String).decodeToString()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalEncodingApi::class)
|
|
||||||
private fun base64DecodeToByteArray(base64String: String): ByteArray =
|
|
||||||
Base64.decode(base64String)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ fun NavController.navigateToAbout(navOptions: NavOptions? = null) =
|
|||||||
navigate(ABOUT_ROUTE, navOptions)
|
navigate(ABOUT_ROUTE, navOptions)
|
||||||
|
|
||||||
fun NavGraphBuilder.aboutScreen(
|
fun NavGraphBuilder.aboutScreen(
|
||||||
onBackClick: () -> Unit,
|
onNavigateToLibraries: () -> Unit,
|
||||||
onNavigateToLibraries: () -> Unit
|
onBackClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
composable(
|
composable(
|
||||||
route = ABOUT_ROUTE,
|
route = ABOUT_ROUTE,
|
||||||
@@ -28,8 +28,8 @@ fun NavGraphBuilder.aboutScreen(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
AboutRoute(
|
AboutRoute(
|
||||||
onBackClick = onBackClick,
|
onNavigateToLibraries = onNavigateToLibraries,
|
||||||
onNavigateToLibraries = onNavigateToLibraries
|
onBackClick = onBackClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ fun OxygenNavHost(
|
|||||||
onBackClick = navController::popBackStack
|
onBackClick = navController::popBackStack
|
||||||
)
|
)
|
||||||
toolsScreen(
|
toolsScreen(
|
||||||
|
onNavigateToToolView = navController::navigateToToolView,
|
||||||
onShowSnackbar = onShowSnackbar,
|
onShowSnackbar = onShowSnackbar,
|
||||||
handleOnCanScrollChange = handleOnCanScrollChange
|
handleOnCanScrollChange = handleOnCanScrollChange
|
||||||
)
|
)
|
||||||
|
toolViewScreen(
|
||||||
|
onBackClick = navController::popBackStack
|
||||||
|
)
|
||||||
starScreen(
|
starScreen(
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptionsBuilder
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.tool.ToolViewRoute
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import kotlin.text.Charsets.UTF_8
|
||||||
|
|
||||||
|
private val URL_CHARACTER_ENCODING = UTF_8.name()
|
||||||
|
|
||||||
|
internal const val USER_NAME_ARG = "username"
|
||||||
|
internal const val TOOL_ID_ARG = "toolId"
|
||||||
|
const val TOOL_VIEW_ROUTE = "tool_view_route"
|
||||||
|
|
||||||
|
internal class ToolViewArgs(val username: String, val toolId: String) {
|
||||||
|
constructor(savedStateHandle: SavedStateHandle) :
|
||||||
|
this(
|
||||||
|
URLDecoder.decode(
|
||||||
|
checkNotNull(savedStateHandle[USER_NAME_ARG]),
|
||||||
|
URL_CHARACTER_ENCODING
|
||||||
|
),
|
||||||
|
URLDecoder.decode(
|
||||||
|
checkNotNull(savedStateHandle[TOOL_ID_ARG]),
|
||||||
|
URL_CHARACTER_ENCODING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavController.navigateToToolView(
|
||||||
|
username: String,
|
||||||
|
toolId: String,
|
||||||
|
navOptions: NavOptionsBuilder.() -> Unit = {}
|
||||||
|
) {
|
||||||
|
val encodedUsername = URLEncoder.encode(username, URL_CHARACTER_ENCODING)
|
||||||
|
val encodedToolId = URLEncoder.encode(toolId, URL_CHARACTER_ENCODING)
|
||||||
|
val newRoute = "$TOOL_VIEW_ROUTE/$encodedUsername/$encodedToolId"
|
||||||
|
navigate(newRoute) {
|
||||||
|
navOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.toolViewScreen(
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
route = "${TOOL_VIEW_ROUTE}/{$USER_NAME_ARG}/{$TOOL_ID_ARG}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument(USER_NAME_ARG) { type = NavType.StringType },
|
||||||
|
navArgument(TOOL_ID_ARG) { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ToolViewRoute(
|
||||||
|
onBackClick = onBackClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ const val TOOLS_ROUTE = "tools_route"
|
|||||||
fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions)
|
fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions)
|
||||||
|
|
||||||
fun NavGraphBuilder.toolsScreen(
|
fun NavGraphBuilder.toolsScreen(
|
||||||
|
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
onShowSnackbar: suspend (String, String?) -> Boolean,
|
||||||
handleOnCanScrollChange: (Boolean) -> Unit
|
handleOnCanScrollChange: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -18,6 +19,7 @@ fun NavGraphBuilder.toolsScreen(
|
|||||||
route = TOOLS_ROUTE
|
route = TOOLS_ROUTE
|
||||||
) {
|
) {
|
||||||
ToolsRoute(
|
ToolsRoute(
|
||||||
|
onNavigateToToolView = onNavigateToToolView,
|
||||||
onShowSnackbar = onShowSnackbar,
|
onShowSnackbar = onShowSnackbar,
|
||||||
handleOnCanScrollChange = handleOnCanScrollChange
|
handleOnCanScrollChange = handleOnCanScrollChange
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ data class ToolDataVo(
|
|||||||
val data: String,
|
val data: String,
|
||||||
|
|
||||||
@Serializable(LocalDateTimeSerializer::class)
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
val createTime: LocalDateTime,
|
val createTime: LocalDateTime? = null,
|
||||||
|
|
||||||
@Serializable(LocalDateTimeSerializer::class)
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
val updateTime: LocalDateTime
|
val updateTime: LocalDateTime? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ internal class RetrofitOxygenNetwork @Inject constructor(
|
|||||||
): ResponseResult<PageVo<ToolVo>> =
|
): ResponseResult<PageVo<ToolVo>> =
|
||||||
networkApi.getStore(searchValue = searchValue, currentPage = currentPage)
|
networkApi.getStore(searchValue = searchValue, currentPage = currentPage)
|
||||||
|
|
||||||
override suspend fun detail(
|
override fun detail(
|
||||||
username: String,
|
username: String,
|
||||||
toolId: String,
|
toolId: String,
|
||||||
ver: String,
|
ver: String,
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import top.fatweb.oxygen.toolbox.model.Result
|
|||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
||||||
|
|
||||||
interface ToolRepository {
|
interface ToolRepository {
|
||||||
|
val toolViewTemplate: Flow<String>
|
||||||
|
|
||||||
suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<Tool>>
|
suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<Tool>>
|
||||||
|
|
||||||
suspend fun detail(
|
fun detail(
|
||||||
username: String,
|
username: String,
|
||||||
toolId: String,
|
toolId: String,
|
||||||
ver: String = "latest",
|
ver: String = "latest",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.paging.PagingData
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
|
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.ToolDataSource
|
||||||
import top.fatweb.oxygen.toolbox.model.Result
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
import top.fatweb.oxygen.toolbox.model.asExternalModel
|
import top.fatweb.oxygen.toolbox.model.asExternalModel
|
||||||
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
||||||
@@ -19,8 +20,11 @@ import javax.inject.Inject
|
|||||||
private const val PAGE_SIZE = 20
|
private const val PAGE_SIZE = 20
|
||||||
|
|
||||||
internal class NetworkToolRepository @Inject constructor(
|
internal class NetworkToolRepository @Inject constructor(
|
||||||
private val oxygenNetworkDataSource: OxygenNetworkDataSource
|
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
|
||||||
|
private val toolDataSource: ToolDataSource
|
||||||
) : ToolRepository {
|
) : ToolRepository {
|
||||||
|
override val toolViewTemplate: Flow<String>
|
||||||
|
get() = toolDataSource.toolViewTemplate
|
||||||
|
|
||||||
override suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<Tool>> =
|
override suspend fun getStore(searchValue: String, currentPage: Int): Flow<PagingData<Tool>> =
|
||||||
Pager(
|
Pager(
|
||||||
@@ -28,7 +32,7 @@ internal class NetworkToolRepository @Inject constructor(
|
|||||||
pagingSourceFactory = { ToolStorePagingSource(oxygenNetworkDataSource, searchValue) }
|
pagingSourceFactory = { ToolStorePagingSource(oxygenNetworkDataSource, searchValue) }
|
||||||
).flow
|
).flow
|
||||||
|
|
||||||
override suspend fun detail(
|
override fun detail(
|
||||||
username: String,
|
username: String,
|
||||||
toolId: String,
|
toolId: String,
|
||||||
ver: String,
|
ver: String,
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -43,7 +42,9 @@ import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AboutRoute(
|
internal fun AboutRoute(
|
||||||
modifier: Modifier = Modifier, onBackClick: () -> Unit, onNavigateToLibraries: () -> Unit
|
modifier: Modifier = Modifier,
|
||||||
|
onNavigateToLibraries: () -> Unit,
|
||||||
|
onBackClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
AboutScreen(
|
AboutScreen(
|
||||||
modifier = modifier.safeDrawingPadding(),
|
modifier = modifier.safeDrawingPadding(),
|
||||||
@@ -55,7 +56,9 @@ internal fun AboutRoute(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AboutScreen(
|
internal fun AboutScreen(
|
||||||
modifier: Modifier = Modifier, onBackClick: () -> Unit, onNavigateToLibraries: () -> Unit
|
modifier: Modifier = Modifier,
|
||||||
|
onNavigateToLibraries: () -> Unit,
|
||||||
|
onBackClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val topAppBarScrollBehavior =
|
val topAppBarScrollBehavior =
|
||||||
@@ -66,7 +69,19 @@ internal fun AboutScreen(
|
|||||||
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
topBar = {
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.windowInsetsPadding(
|
||||||
|
WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Horizontal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
OxygenTopAppBar(
|
OxygenTopAppBar(
|
||||||
scrollBehavior = topAppBarScrollBehavior,
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
titleRes = R.string.feature_settings_more_about,
|
titleRes = R.string.feature_settings_more_about,
|
||||||
@@ -78,21 +93,6 @@ internal fun AboutScreen(
|
|||||||
),
|
),
|
||||||
onNavigationClick = onBackClick
|
onNavigationClick = onBackClick
|
||||||
)
|
)
|
||||||
}
|
|
||||||
) { padding ->
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight()
|
|
||||||
.padding(padding)
|
|
||||||
.consumeWindowInsets(padding)
|
|
||||||
.windowInsetsPadding(
|
|
||||||
WindowInsets.safeDrawing.only(
|
|
||||||
WindowInsetsSides.Horizontal
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier.height(64.dp)
|
modifier = Modifier.height(64.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -29,11 +27,9 @@ import androidx.compose.foundation.layout.safeDrawingPadding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -112,7 +108,8 @@ internal fun LibrariesScreen(
|
|||||||
var dialogContent by remember { mutableStateOf("") }
|
var dialogContent by remember { mutableStateOf("") }
|
||||||
var dialogUrl by remember { mutableStateOf("") }
|
var dialogUrl by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(canScroll = { state.canScrollForward })
|
val topAppBarScrollBehavior =
|
||||||
|
TopAppBarDefaults.enterAlwaysScrollBehavior(canScroll = { state.canScrollForward })
|
||||||
|
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
|
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
|
||||||
|
|
||||||
@@ -124,7 +121,18 @@ internal fun LibrariesScreen(
|
|||||||
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
topBar = {
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.windowInsetsPadding(
|
||||||
|
WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Horizontal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
OxygenTopAppBar(
|
OxygenTopAppBar(
|
||||||
scrollBehavior = topAppBarScrollBehavior,
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
titleRes = R.string.feature_settings_open_source_license,
|
titleRes = R.string.feature_settings_open_source_license,
|
||||||
@@ -153,88 +161,73 @@ internal fun LibrariesScreen(
|
|||||||
onSearch("")
|
onSearch("")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
Box(modifier = Modifier) {
|
||||||
) { padding ->
|
when (librariesScreenUiState) {
|
||||||
Box(
|
LibrariesScreenUiState.Loading -> {
|
||||||
modifier
|
Column(
|
||||||
.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.padding(padding)
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
.consumeWindowInsets(padding)
|
verticalArrangement = Arrangement.Center
|
||||||
.windowInsetsPadding(
|
) {
|
||||||
WindowInsets.safeDrawing.only(
|
val angle by infiniteTransition.animateFloat(
|
||||||
WindowInsetsSides.Horizontal
|
initialValue = 0F,
|
||||||
)
|
targetValue = 360F,
|
||||||
)
|
animationSpec = infiniteRepeatable(
|
||||||
) {
|
animation = tween(800, easing = Ease),
|
||||||
when (librariesScreenUiState) {
|
), label = "angle"
|
||||||
LibrariesScreenUiState.Loading -> {Column(
|
)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Icon(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.Center
|
.size(32.dp)
|
||||||
) {
|
.graphicsLayer { rotationZ = angle },
|
||||||
val angle by infiniteTransition.animateFloat(
|
imageVector = OxygenIcons.Loading,
|
||||||
initialValue = 0F,
|
contentDescription = ""
|
||||||
targetValue = 360F,
|
)
|
||||||
animationSpec = infiniteRepeatable(
|
|
||||||
animation = tween(800, easing = Ease),
|
|
||||||
), label = "angle"
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(32.dp)
|
|
||||||
.graphicsLayer { rotationZ = angle },
|
|
||||||
imageVector = OxygenIcons.Loading,
|
|
||||||
contentDescription = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LibrariesScreenUiState.Nothing -> {
|
|
||||||
Text(text = "Nothing")
|
|
||||||
}
|
|
||||||
|
|
||||||
LibrariesScreenUiState.NotFound -> {
|
|
||||||
Text(text = "Not Found")
|
|
||||||
}
|
|
||||||
|
|
||||||
is LibrariesScreenUiState.Success -> {
|
|
||||||
val handleOnClickLicense = { key: String ->
|
|
||||||
val license = librariesScreenUiState.dependencies.licenses[key]
|
|
||||||
if (license != null) {
|
|
||||||
showDialog = true
|
|
||||||
dialogTitle = license.name
|
|
||||||
dialogContent = license.content ?: ""
|
|
||||||
dialogUrl = license.url ?: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyVerticalStaggeredGrid(
|
LibrariesScreenUiState.Nothing -> {
|
||||||
columns = StaggeredGridCells.Adaptive(300.dp),
|
Text(text = "Nothing")
|
||||||
contentPadding = PaddingValues(16.dp),
|
}
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
verticalItemSpacing = 24.dp,
|
LibrariesScreenUiState.NotFound -> {
|
||||||
state = state
|
Text(text = "Not Found")
|
||||||
) {
|
}
|
||||||
librariesPanel(
|
|
||||||
librariesScreenUiState = librariesScreenUiState,
|
is LibrariesScreenUiState.Success -> {
|
||||||
onClickLicense = handleOnClickLicense
|
val handleOnClickLicense = { key: String ->
|
||||||
|
val license = librariesScreenUiState.dependencies.licenses[key]
|
||||||
|
if (license != null) {
|
||||||
|
showDialog = true
|
||||||
|
dialogTitle = license.name
|
||||||
|
dialogContent = license.content ?: ""
|
||||||
|
dialogUrl = license.url ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyVerticalStaggeredGrid(
|
||||||
|
columns = StaggeredGridCells.Adaptive(300.dp),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalItemSpacing = 24.dp,
|
||||||
|
state = state
|
||||||
|
) {
|
||||||
|
librariesPanel(
|
||||||
|
librariesScreenUiState = librariesScreenUiState,
|
||||||
|
onClickLicense = handleOnClickLicense
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.DraggableScrollbar(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.windowInsetsPadding(WindowInsets.systemBars)
|
||||||
|
.padding(horizontal = 2.dp)
|
||||||
|
.align(Alignment.CenterEnd),
|
||||||
|
state = scrollbarState, orientation = Orientation.Vertical,
|
||||||
|
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
|
||||||
)
|
)
|
||||||
|
|
||||||
item(span = StaggeredGridItemSpan.FullLine) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.DraggableScrollbar(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.windowInsetsPadding(WindowInsets.systemBars)
|
|
||||||
.padding(horizontal = 2.dp)
|
|
||||||
.align(Alignment.CenterEnd),
|
|
||||||
state = scrollbarState, orientation = Orientation.Vertical,
|
|
||||||
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import android.R as androidR
|
|||||||
fun OxygenTopAppBar(
|
fun OxygenTopAppBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
@StringRes titleRes: Int,
|
@StringRes titleRes: Int? = null,
|
||||||
navigationIcon: ImageVector? = null,
|
navigationIcon: ImageVector? = null,
|
||||||
navigationIconContentDescription: String? = null,
|
navigationIconContentDescription: String? = null,
|
||||||
actionIcon: ImageVector? = null,
|
actionIcon: ImageVector? = null,
|
||||||
@@ -120,7 +120,7 @@ fun OxygenTopAppBar(
|
|||||||
if ("\n" !in it) onQueryChange(it)
|
if ("\n" !in it) onQueryChange(it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else Text(stringResource(titleRes))
|
else if (titleRes != null) Text(stringResource(titleRes))
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
navigationIcon?.let {
|
navigationIcon?.let {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ fun ToolInfo(
|
|||||||
toolDesc: String
|
toolDesc: String
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.tool
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.animation.core.Ease
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.kevinnzou.web.WebView
|
||||||
|
import com.kevinnzou.web.rememberWebViewStateWithHTMLData
|
||||||
|
import top.fatweb.oxygen.toolbox.R
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.Loading
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ToolViewRoute(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: ToolViewScreenViewModel = hiltViewModel(),
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val toolViewUiState by viewModel.toolViewUiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
ToolViewScreen(
|
||||||
|
modifier = modifier.safeDrawingPadding(),
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
toolViewUiState = toolViewUiState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun ToolViewScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
toolViewUiState: ToolViewUiState
|
||||||
|
) {
|
||||||
|
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.windowInsetsPadding(WindowInsets(0, 0, 0, 0)),
|
||||||
|
) {
|
||||||
|
OxygenTopAppBar(
|
||||||
|
modifier = Modifier.zIndex(100f),
|
||||||
|
navigationIcon = OxygenIcons.Back,
|
||||||
|
navigationIconContentDescription = stringResource(R.string.core_back),
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
scrolledContainerColor = Color.Transparent
|
||||||
|
),
|
||||||
|
onNavigationClick = onBackClick
|
||||||
|
)
|
||||||
|
when (toolViewUiState) {
|
||||||
|
ToolViewUiState.Loading -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
val angle by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0F,
|
||||||
|
targetValue = 360F,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(800, easing = Ease),
|
||||||
|
), label = "angle"
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.graphicsLayer { rotationZ = angle },
|
||||||
|
imageVector = OxygenIcons.Loading,
|
||||||
|
contentDescription = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolViewUiState.Error -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.feature_tools_can_not_open))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ToolViewUiState.Success -> {
|
||||||
|
val webViewState = rememberWebViewStateWithHTMLData(
|
||||||
|
data = toolViewUiState.htmlData,
|
||||||
|
)
|
||||||
|
WebView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = webViewState,
|
||||||
|
onCreated = {
|
||||||
|
it.settings.javaScriptEnabled = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.tool
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
|
import top.fatweb.oxygen.toolbox.navigation.ToolViewArgs
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.util.decodeToStringWithZip
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ToolViewScreenViewModel @Inject constructor(
|
||||||
|
toolRepository: ToolRepository,
|
||||||
|
savedStateHandle: SavedStateHandle
|
||||||
|
) : ViewModel() {
|
||||||
|
private val toolViewArgs = ToolViewArgs(savedStateHandle)
|
||||||
|
val username = toolViewArgs.username
|
||||||
|
val toolId = toolViewArgs.toolId
|
||||||
|
|
||||||
|
val toolViewUiState: StateFlow<ToolViewUiState> = toolViewUiState(
|
||||||
|
username = username,
|
||||||
|
toolId = toolId,
|
||||||
|
toolRepository = toolRepository
|
||||||
|
)
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
initialValue = ToolViewUiState.Loading,
|
||||||
|
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toolViewUiState(
|
||||||
|
username: String,
|
||||||
|
toolId: String,
|
||||||
|
toolRepository: ToolRepository
|
||||||
|
): Flow<ToolViewUiState> {
|
||||||
|
val result = toolRepository.detail(
|
||||||
|
username = username,
|
||||||
|
toolId = toolId
|
||||||
|
)
|
||||||
|
val toolViewTemplate = toolRepository.toolViewTemplate
|
||||||
|
|
||||||
|
return combine(result, toolViewTemplate, ::Pair).map { (result, toolViewTemplate) ->
|
||||||
|
when (result) {
|
||||||
|
is Result.Success -> {
|
||||||
|
val dist = result.data.dist!!
|
||||||
|
val base = result.data.base!!
|
||||||
|
ToolViewUiState.Success(
|
||||||
|
processHtml(
|
||||||
|
toolViewTemplate = toolViewTemplate,
|
||||||
|
distBase64 = dist,
|
||||||
|
baseBase64 = base
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Result.Loading -> ToolViewUiState.Loading
|
||||||
|
is Result.Error -> {
|
||||||
|
Log.e("TAG", "toolViewUiState: can not load tool", result.exception)
|
||||||
|
|
||||||
|
ToolViewUiState.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
is Result.Fail -> ToolViewUiState.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ToolViewUiState {
|
||||||
|
data class Success(val htmlData: String) : ToolViewUiState
|
||||||
|
|
||||||
|
data object Error : ToolViewUiState
|
||||||
|
|
||||||
|
data object Loading : ToolViewUiState
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
fun processHtml(toolViewTemplate: String, distBase64: String, baseBase64: String): String {
|
||||||
|
val dist = Base64.decodeToStringWithZip(distBase64)
|
||||||
|
val base = Base64.decodeToStringWithZip(baseBase64)
|
||||||
|
|
||||||
|
return toolViewTemplate.replace("{{replace_code}}", "$dist\n$base")
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui.tool
|
package top.fatweb.oxygen.toolbox.ui.tool
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.ReportDrawnWhen
|
import androidx.activity.compose.ReportDrawnWhen
|
||||||
import androidx.compose.animation.core.Ease
|
import androidx.compose.animation.core.Ease
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
@@ -35,7 +34,6 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
@@ -52,6 +50,7 @@ import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
|
|||||||
internal fun ToolsRoute(
|
internal fun ToolsRoute(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: ToolsScreenViewModel = hiltViewModel(),
|
viewModel: ToolsScreenViewModel = hiltViewModel(),
|
||||||
|
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
onShowSnackbar: suspend (String, String?) -> Boolean,
|
||||||
handleOnCanScrollChange: (Boolean) -> Unit
|
handleOnCanScrollChange: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -59,6 +58,7 @@ internal fun ToolsRoute(
|
|||||||
|
|
||||||
ToolsScreen(
|
ToolsScreen(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
onNavigateToToolView = onNavigateToToolView,
|
||||||
onShowSnackbar = onShowSnackbar,
|
onShowSnackbar = onShowSnackbar,
|
||||||
handleOnCanScrollChange = handleOnCanScrollChange,
|
handleOnCanScrollChange = handleOnCanScrollChange,
|
||||||
toolStorePagingItems = toolStorePagingItems
|
toolStorePagingItems = toolStorePagingItems
|
||||||
@@ -68,11 +68,11 @@ internal fun ToolsRoute(
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun ToolsScreen(
|
internal fun ToolsScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
onNavigateToToolView: (username: String, toolId: String) -> Unit,
|
||||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
onShowSnackbar: suspend (String, String?) -> Boolean,
|
||||||
handleOnCanScrollChange: (Boolean) -> Unit,
|
handleOnCanScrollChange: (Boolean) -> Unit,
|
||||||
toolStorePagingItems: LazyPagingItems<Tool>
|
toolStorePagingItems: LazyPagingItems<Tool>
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val isToolLoading =
|
val isToolLoading =
|
||||||
toolStorePagingItems.loadState.refresh is LoadState.Loading
|
toolStorePagingItems.loadState.refresh is LoadState.Loading
|
||||||
|| toolStorePagingItems.loadState.append is LoadState.Loading
|
|| toolStorePagingItems.loadState.append is LoadState.Loading
|
||||||
@@ -86,10 +86,6 @@ internal fun ToolsScreen(
|
|||||||
|
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
|
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
|
||||||
|
|
||||||
val handleOnClickToolCard = { username: String, toolId: String ->
|
|
||||||
Toast.makeText(context, "$username:$toolId", Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(state.canScrollForward) {
|
LaunchedEffect(state.canScrollForward) {
|
||||||
handleOnCanScrollChange(state.canScrollForward)
|
handleOnCanScrollChange(state.canScrollForward)
|
||||||
}
|
}
|
||||||
@@ -106,7 +102,7 @@ internal fun ToolsScreen(
|
|||||||
|
|
||||||
toolsPanel(
|
toolsPanel(
|
||||||
toolStorePagingItems = toolStorePagingItems,
|
toolStorePagingItems = toolStorePagingItems,
|
||||||
onClickToolCard = handleOnClickToolCard
|
onClickToolCard = onNavigateToToolView
|
||||||
)
|
)
|
||||||
|
|
||||||
item(span = StaggeredGridItemSpan.FullLine) {
|
item(span = StaggeredGridItemSpan.FullLine) {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.util
|
||||||
|
|
||||||
|
import java.util.zip.Inflater
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
fun Base64.decodeToString(base64String: String): String =
|
||||||
|
this.decode(base64String).decodeToString()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
fun Base64.decodeToByteArray(base64String: String): ByteArray =
|
||||||
|
this.decode(base64String)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
fun Base64.decodeToStringWithZip(base64String: String): String {
|
||||||
|
val binary = this.decode(base64String).toString(Charsets.ISO_8859_1)
|
||||||
|
|
||||||
|
// zlib header (x78), level 9 (xDA)
|
||||||
|
if (binary.startsWith("\u0078\u00DA")) {
|
||||||
|
val byteArray = binary.toByteArray(Charsets.ISO_8859_1)
|
||||||
|
val inflater = Inflater().apply {
|
||||||
|
setInput(byteArray)
|
||||||
|
}
|
||||||
|
val uncompressed = ByteArray(byteArray.size * 10)
|
||||||
|
val resultLength = inflater.inflate(uncompressed)
|
||||||
|
inflater.end()
|
||||||
|
|
||||||
|
return String(uncompressed, 0, resultLength, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -35,4 +35,5 @@
|
|||||||
<string name="feature_settings_top_app_bar_action_icon_description">更多</string>
|
<string name="feature_settings_top_app_bar_action_icon_description">更多</string>
|
||||||
<string name="feature_settings_top_app_bar_navigation_icon_description">搜索</string>
|
<string name="feature_settings_top_app_bar_navigation_icon_description">搜索</string>
|
||||||
<string name="feature_tools_description">简介</string>
|
<string name="feature_tools_description">简介</string>
|
||||||
|
<string name="feature_tools_can_not_open">⚠️ 无法打开工具</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -36,4 +36,5 @@
|
|||||||
<string name="feature_settings_top_app_bar_action_icon_description">More</string>
|
<string name="feature_settings_top_app_bar_action_icon_description">More</string>
|
||||||
<string name="feature_settings_top_app_bar_navigation_icon_description">Search</string>
|
<string name="feature_settings_top_app_bar_navigation_icon_description">Search</string>
|
||||||
<string name="feature_tools_description">Desc</string>
|
<string name="feature_tools_description">Desc</string>
|
||||||
|
<string name="feature_tools_can_not_open">⚠️ Can not open the tool</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -30,6 +30,7 @@ retrofit = "2.9.0"
|
|||||||
retrofitKotlinxSerializationJson = "1.0.0"
|
retrofitKotlinxSerializationJson = "1.0.0"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
androidsvg = "1.4"
|
androidsvg = "1.4"
|
||||||
|
webviewCompose = "0.33.6"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
@@ -91,3 +92,4 @@ okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor",
|
|||||||
paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" }
|
paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" }
|
||||||
paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }
|
paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }
|
||||||
androidsvg-aar = { group = "com.caverock", name = "androidsvg-aar", version.ref = "androidsvg" }
|
androidsvg-aar = { group = "com.caverock", name = "androidsvg-aar", version.ref = "androidsvg" }
|
||||||
|
compose-webview = { group = "io.github.kevinnzou", name = "compose-webview", version.ref = "webviewCompose" }
|
||||||
|
|||||||
Reference in New Issue
Block a user