diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9433104..99b93ff 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -169,4 +169,5 @@ dependencies {
implementation(libs.paging.runtime)
implementation(libs.paging.compose)
implementation(libs.androidsvg.aar)
+ implementation(libs.compose.webview)
}
\ No newline at end of file
diff --git a/app/src/main/assets/template/tool-view.html b/app/src/main/assets/template/tool-view.html
new file mode 100644
index 0000000..1a12873
--- /dev/null
+++ b/app/src/main/assets/template/tool-view.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Preview
+
+
+
+
+
+
+
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/network/OxygenNetworkDataSource.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/network/OxygenNetworkDataSource.kt
index 2a0c8e7..0071850 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/network/OxygenNetworkDataSource.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/network/OxygenNetworkDataSource.kt
@@ -13,7 +13,7 @@ interface OxygenNetworkDataSource {
currentPage: Int = 1
): ResponseResult>
- suspend fun detail(
+ fun detail(
username: String,
toolId: String,
ver: String = "latest",
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/ToolDataSource.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/ToolDataSource.kt
index 7766eb1..70bd6f5 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/ToolDataSource.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/data/tool/ToolDataSource.kt
@@ -1,7 +1,24 @@
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
-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)
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt
index 6dd08ad..ffc293f 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt
@@ -22,6 +22,8 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.graphics.drawable.toBitmap
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.ExperimentalEncodingApi
@@ -42,22 +44,16 @@ object OxygenIcons {
val Time = Icons.Default.AccessTime
val Tool = Icons.Default.Build
+ @OptIn(ExperimentalEncodingApi::class)
fun fromSvgBase64(base64String: String): ImageBitmap {
- val svg = SVG.getFromString(base64DecodeToString(base64String))
+ val svg = SVG.getFromString(Base64.decodeToString(base64String))
val drawable = PictureDrawable(svg.renderToPicture())
return drawable.toBitmap().asImageBitmap()
}
+ @OptIn(ExperimentalEncodingApi::class)
fun fromPngBase64(base64String: String): ImageBitmap {
- val byteArray = base64DecodeToByteArray(base64String)
+ val byteArray = Base64.decodeToByteArray(base64String)
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)
}
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/AboutNavigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/AboutNavigation.kt
index 22a052e..4dc9db0 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/AboutNavigation.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/AboutNavigation.kt
@@ -14,8 +14,8 @@ fun NavController.navigateToAbout(navOptions: NavOptions? = null) =
navigate(ABOUT_ROUTE, navOptions)
fun NavGraphBuilder.aboutScreen(
- onBackClick: () -> Unit,
- onNavigateToLibraries: () -> Unit
+ onNavigateToLibraries: () -> Unit,
+ onBackClick: () -> Unit
) {
composable(
route = ABOUT_ROUTE,
@@ -28,8 +28,8 @@ fun NavGraphBuilder.aboutScreen(
}
) {
AboutRoute(
- onBackClick = onBackClick,
- onNavigateToLibraries = onNavigateToLibraries
+ onNavigateToLibraries = onNavigateToLibraries,
+ onBackClick = onBackClick
)
}
}
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt
index a845ce9..191879a 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt
@@ -30,9 +30,13 @@ fun OxygenNavHost(
onBackClick = navController::popBackStack
)
toolsScreen(
+ onNavigateToToolView = navController::navigateToToolView,
onShowSnackbar = onShowSnackbar,
handleOnCanScrollChange = handleOnCanScrollChange
)
+ toolViewScreen(
+ onBackClick = navController::popBackStack
+ )
starScreen(
)
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolViewNavigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolViewNavigation.kt
new file mode 100644
index 0000000..6d5fea5
--- /dev/null
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolViewNavigation.kt
@@ -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
+ )
+ }
+}
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt
index c3cae51..ce8f511 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt
@@ -11,6 +11,7 @@ const val TOOLS_ROUTE = "tools_route"
fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions)
fun NavGraphBuilder.toolsScreen(
+ onNavigateToToolView: (username: String, toolId: String) -> Unit,
onShowSnackbar: suspend (String, String?) -> Boolean,
handleOnCanScrollChange: (Boolean) -> Unit
) {
@@ -18,6 +19,7 @@ fun NavGraphBuilder.toolsScreen(
route = TOOLS_ROUTE
) {
ToolsRoute(
+ onNavigateToToolView = onNavigateToToolView,
onShowSnackbar = onShowSnackbar,
handleOnCanScrollChange = handleOnCanScrollChange
)
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolDataVo.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolDataVo.kt
index 1508780..396a7bd 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolDataVo.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/model/ToolDataVo.kt
@@ -11,8 +11,8 @@ data class ToolDataVo(
val data: String,
@Serializable(LocalDateTimeSerializer::class)
- val createTime: LocalDateTime,
+ val createTime: LocalDateTime? = null,
@Serializable(LocalDateTimeSerializer::class)
- val updateTime: LocalDateTime
+ val updateTime: LocalDateTime? = null
)
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/retrofit/RetrofitOxygenNetwork.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/retrofit/RetrofitOxygenNetwork.kt
index cf4a27b..3c4b51e 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/retrofit/RetrofitOxygenNetwork.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/retrofit/RetrofitOxygenNetwork.kt
@@ -58,7 +58,7 @@ internal class RetrofitOxygenNetwork @Inject constructor(
): ResponseResult> =
networkApi.getStore(searchValue = searchValue, currentPage = currentPage)
- override suspend fun detail(
+ override fun detail(
username: String,
toolId: String,
ver: String,
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/ToolRepository.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/ToolRepository.kt
index 1d32b4a..5063b3d 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/ToolRepository.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/ToolRepository.kt
@@ -6,9 +6,11 @@ import top.fatweb.oxygen.toolbox.model.Result
import top.fatweb.oxygen.toolbox.model.tool.Tool
interface ToolRepository {
+ val toolViewTemplate: Flow
+
suspend fun getStore(searchValue: String, currentPage: Int): Flow>
- suspend fun detail(
+ fun detail(
username: String,
toolId: String,
ver: String = "latest",
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkToolRepository.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkToolRepository.kt
index dbfd6a1..a5cd2d0 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkToolRepository.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/tool/impl/NetworkToolRepository.kt
@@ -6,6 +6,7 @@ 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.data.tool.ToolDataSource
import top.fatweb.oxygen.toolbox.model.Result
import top.fatweb.oxygen.toolbox.model.asExternalModel
import top.fatweb.oxygen.toolbox.model.tool.Tool
@@ -19,8 +20,11 @@ import javax.inject.Inject
private const val PAGE_SIZE = 20
internal class NetworkToolRepository @Inject constructor(
- private val oxygenNetworkDataSource: OxygenNetworkDataSource
+ private val oxygenNetworkDataSource: OxygenNetworkDataSource,
+ private val toolDataSource: ToolDataSource
) : ToolRepository {
+ override val toolViewTemplate: Flow
+ get() = toolDataSource.toolViewTemplate
override suspend fun getStore(searchValue: String, currentPage: Int): Flow> =
Pager(
@@ -28,7 +32,7 @@ internal class NetworkToolRepository @Inject constructor(
pagingSourceFactory = { ToolStorePagingSource(oxygenNetworkDataSource, searchValue) }
).flow
- override suspend fun detail(
+ override fun detail(
username: String,
toolId: String,
ver: String,
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/AboutScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/AboutScreen.kt
index dc301bd..62f0ec7 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/AboutScreen.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/AboutScreen.kt
@@ -7,8 +7,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
@@ -43,7 +42,9 @@ import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
@Composable
internal fun AboutRoute(
- modifier: Modifier = Modifier, onBackClick: () -> Unit, onNavigateToLibraries: () -> Unit
+ modifier: Modifier = Modifier,
+ onNavigateToLibraries: () -> Unit,
+ onBackClick: () -> Unit
) {
AboutScreen(
modifier = modifier.safeDrawingPadding(),
@@ -55,7 +56,9 @@ internal fun AboutRoute(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun AboutScreen(
- modifier: Modifier = Modifier, onBackClick: () -> Unit, onNavigateToLibraries: () -> Unit
+ modifier: Modifier = Modifier,
+ onNavigateToLibraries: () -> Unit,
+ onBackClick: () -> Unit
) {
val scrollState = rememberScrollState()
val topAppBarScrollBehavior =
@@ -66,7 +69,19 @@ internal fun AboutScreen(
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
containerColor = Color.Transparent,
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(
scrollBehavior = topAppBarScrollBehavior,
titleRes = R.string.feature_settings_more_about,
@@ -78,21 +93,6 @@ internal fun AboutScreen(
),
onNavigationClick = onBackClick
)
- }
- ) { padding ->
- Column(
- modifier = modifier
- .fillMaxWidth()
- .fillMaxHeight()
- .padding(padding)
- .consumeWindowInsets(padding)
- .windowInsetsPadding(
- WindowInsets.safeDrawing.only(
- WindowInsetsSides.Horizontal
- )
- )
- .verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally
- ) {
Spacer(
modifier = Modifier.height(64.dp)
)
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/LibrariesScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/LibrariesScreen.kt
index 7db70dc..2b1ee37 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/LibrariesScreen.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/LibrariesScreen.kt
@@ -14,13 +14,11 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.only
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.systemBars
import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
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.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -112,7 +108,8 @@ internal fun LibrariesScreen(
var dialogContent 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")
@@ -124,7 +121,18 @@ internal fun LibrariesScreen(
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
containerColor = Color.Transparent,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
- topBar = {
+ ) { padding ->
+ Column(
+ modifier
+ .fillMaxWidth()
+ .padding(padding)
+ .consumeWindowInsets(padding)
+ .windowInsetsPadding(
+ WindowInsets.safeDrawing.only(
+ WindowInsetsSides.Horizontal
+ )
+ )
+ ) {
OxygenTopAppBar(
scrollBehavior = topAppBarScrollBehavior,
titleRes = R.string.feature_settings_open_source_license,
@@ -153,88 +161,73 @@ internal fun LibrariesScreen(
onSearch("")
}
)
- }
- ) { padding ->
- Box(
- modifier
- .fillMaxWidth()
- .padding(padding)
- .consumeWindowInsets(padding)
- .windowInsetsPadding(
- WindowInsets.safeDrawing.only(
- WindowInsetsSides.Horizontal
- )
- )
- ) {
- when (librariesScreenUiState) {
- LibrariesScreenUiState.Loading -> {Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- val angle by infiniteTransition.animateFloat(
- initialValue = 0F,
- targetValue = 360F,
- animationSpec = infiniteRepeatable(
- animation = tween(800, easing = Ease),
- ), label = "angle"
- )
- Icon(
- modifier = Modifier
- .size(32.dp)
- .graphicsLayer { rotationZ = angle },
- imageVector = OxygenIcons.Loading,
- contentDescription = ""
- )
- }
- }
-
- 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 ?: ""
+ Box(modifier = Modifier) {
+ when (librariesScreenUiState) {
+ LibrariesScreenUiState.Loading -> {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ val angle by infiniteTransition.animateFloat(
+ initialValue = 0F,
+ targetValue = 360F,
+ animationSpec = infiniteRepeatable(
+ animation = tween(800, easing = Ease),
+ ), label = "angle"
+ )
+ Icon(
+ modifier = Modifier
+ .size(32.dp)
+ .graphicsLayer { rotationZ = angle },
+ imageVector = OxygenIcons.Loading,
+ contentDescription = ""
+ )
}
}
- 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
+ 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(
+ 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)
- )
}
}
}
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/OxygenTopAppBar.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/OxygenTopAppBar.kt
index 45ce610..ebc516d 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/OxygenTopAppBar.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/OxygenTopAppBar.kt
@@ -46,7 +46,7 @@ import android.R as androidR
fun OxygenTopAppBar(
modifier: Modifier = Modifier,
scrollBehavior: TopAppBarScrollBehavior? = null,
- @StringRes titleRes: Int,
+ @StringRes titleRes: Int? = null,
navigationIcon: ImageVector? = null,
navigationIconContentDescription: String? = null,
actionIcon: ImageVector? = null,
@@ -120,7 +120,7 @@ fun OxygenTopAppBar(
if ("\n" !in it) onQueryChange(it)
}
)
- else Text(stringResource(titleRes))
+ else if (titleRes != null) Text(stringResource(titleRes))
},
navigationIcon = {
navigationIcon?.let {
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt
index f11693a..37eed9e 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt
@@ -108,7 +108,7 @@ fun ToolInfo(
toolDesc: String
) {
Column(
- modifier = modifier,
+ modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreen.kt
new file mode 100644
index 0000000..3c4e85f
--- /dev/null
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreen.kt
@@ -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
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreenViewModel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreenViewModel.kt
new file mode 100644
index 0000000..c16a5e7
--- /dev/null
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolViewScreenViewModel.kt
@@ -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(
+ 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 {
+ 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")
+}
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt
index 02eedc3..a911cd5 100644
--- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt
@@ -1,6 +1,5 @@
package top.fatweb.oxygen.toolbox.ui.tool
-import android.widget.Toast
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.animation.core.Ease
import androidx.compose.animation.core.animateFloat
@@ -35,7 +34,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.LoadState
@@ -52,6 +50,7 @@ import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
internal fun ToolsRoute(
modifier: Modifier = Modifier,
viewModel: ToolsScreenViewModel = hiltViewModel(),
+ onNavigateToToolView: (username: String, toolId: String) -> Unit,
onShowSnackbar: suspend (String, String?) -> Boolean,
handleOnCanScrollChange: (Boolean) -> Unit
) {
@@ -59,6 +58,7 @@ internal fun ToolsRoute(
ToolsScreen(
modifier = modifier,
+ onNavigateToToolView = onNavigateToToolView,
onShowSnackbar = onShowSnackbar,
handleOnCanScrollChange = handleOnCanScrollChange,
toolStorePagingItems = toolStorePagingItems
@@ -68,11 +68,11 @@ internal fun ToolsRoute(
@Composable
internal fun ToolsScreen(
modifier: Modifier = Modifier,
+ onNavigateToToolView: (username: String, toolId: String) -> Unit,
onShowSnackbar: suspend (String, String?) -> Boolean,
handleOnCanScrollChange: (Boolean) -> Unit,
toolStorePagingItems: LazyPagingItems
) {
- val context = LocalContext.current
val isToolLoading =
toolStorePagingItems.loadState.refresh is LoadState.Loading
|| toolStorePagingItems.loadState.append is LoadState.Loading
@@ -86,10 +86,6 @@ internal fun ToolsScreen(
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
- val handleOnClickToolCard = { username: String, toolId: String ->
- Toast.makeText(context, "$username:$toolId", Toast.LENGTH_LONG).show()
- }
-
LaunchedEffect(state.canScrollForward) {
handleOnCanScrollChange(state.canScrollForward)
}
@@ -106,7 +102,7 @@ internal fun ToolsScreen(
toolsPanel(
toolStorePagingItems = toolStorePagingItems,
- onClickToolCard = handleOnClickToolCard
+ onClickToolCard = onNavigateToToolView
)
item(span = StaggeredGridItemSpan.FullLine) {
diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/util/Transcode.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/util/Transcode.kt
new file mode 100644
index 0000000..0b53e4c
--- /dev/null
+++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/util/Transcode.kt
@@ -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 ""
+}
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index b9133c2..5f26bca 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -35,4 +35,5 @@
更多
搜索
简介
+ ⚠️ 无法打开工具
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6b64397..138afc2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -36,4 +36,5 @@
More
Search
Desc
+ ⚠️ Can not open the tool
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a4247cc..531bd50 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -30,6 +30,7 @@ retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "1.0.0"
okhttp = "4.12.0"
androidsvg = "1.4"
+webviewCompose = "0.33.6"
[plugins]
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-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }
androidsvg-aar = { group = "com.caverock", name = "androidsvg-aar", version.ref = "androidsvg" }
+compose-webview = { group = "io.github.kevinnzou", name = "compose-webview", version.ref = "webviewCompose" }