Feat(Tool): Support theme

This commit is contained in:
2024-11-05 11:45:56 +08:00
parent d589df860e
commit 8669a2c9ef
11 changed files with 1910 additions and 29 deletions

View File

@@ -22,4 +22,30 @@ class ToolDataSource @Inject constructor(
}
)
}.flowOn(ioDispatcher)
fun getGlobalJsVariables(isDarkMode: Boolean) = flow {
emit(
context.assets.open(
if (isDarkMode) "template/global-variables-dark.js"
else "template/global-variables-light.js"
)
.bufferedReader()
.use {
it.readText()
}
)
}.flowOn(ioDispatcher)
fun getGlobalCssVariables(isDarkMode: Boolean) = flow {
emit(
context.assets.open(
if (isDarkMode) "template/global-variables-dark.css"
else "template/global-variables-light.css"
)
.bufferedReader()
.use {
it.readText()
}
)
}.flowOn(ioDispatcher)
}

View File

@@ -6,6 +6,10 @@ import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
interface ToolRepository {
val toolViewTemplate: Flow<String>
fun getGlobalJsVariables(isDarkMode: Boolean): Flow<String>
fun getGlobalCssVariables(isDarkMode: Boolean): Flow<String>
fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>>
fun getStarToolsStream(searchValue: String): Flow<List<ToolEntity>>

View File

@@ -14,6 +14,12 @@ class OfflineToolRepository @Inject constructor(
override val toolViewTemplate: Flow<String>
get() = toolDataSource.toolViewTemplate
override fun getGlobalJsVariables(isDarkMode: Boolean): Flow<String> =
toolDataSource.getGlobalJsVariables(isDarkMode)
override fun getGlobalCssVariables(isDarkMode: Boolean): Flow<String> =
toolDataSource.getGlobalCssVariables(isDarkMode)
override fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>> =
toolDao.selectAllTools(searchValue)

View File

@@ -11,7 +11,7 @@ import androidx.core.os.LocaleListCompat
import java.util.Locale
object ResourcesUtils {
private fun getConfiguration(context: Context): Configuration = context.resources.configuration
fun getConfiguration(context: Context): Configuration = context.resources.configuration
fun getAppLocale(context: Context): Locale = getConfiguration(context).locales.get(0)

View File

@@ -220,7 +220,8 @@ private fun Content(
),
factory = {
webViewInstanceState.webView
}
},
captureBackPresses = false
)
}
}

View File

@@ -1,27 +1,38 @@
package top.fatweb.oxygen.toolbox.ui.view
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.webkit.WebView
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import timber.log.Timber
import top.fatweb.oxygen.toolbox.model.Result
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
import top.fatweb.oxygen.toolbox.model.userdata.DarkThemeConfig
import top.fatweb.oxygen.toolbox.navigation.ToolViewArgs
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
import top.fatweb.oxygen.toolbox.util.decodeToStringWithZip
import javax.inject.Inject
import kotlin.io.encoding.Base64
@@ -31,6 +42,7 @@ import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class ToolViewScreenViewModel @Inject constructor(
@ApplicationContext context: Context,
userDataRepository: UserDataRepository,
storeRepository: StoreRepository,
toolRepository: ToolRepository,
savedStateHandle: SavedStateHandle
@@ -46,10 +58,12 @@ class ToolViewScreenViewModel @Inject constructor(
val toolViewUiState: StateFlow<ToolViewUiState> = toolViewUiState(
context = context,
savedStateHandle = savedStateHandle,
username = username,
toolId = toolId,
preview = preview,
userDataRepository = userDataRepository,
storeRepository = storeRepository,
toolRepository = toolRepository,
storeDetailCache = storeDetailCache
@@ -70,11 +84,14 @@ class ToolViewScreenViewModel @Inject constructor(
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun toolViewUiState(
context: Context,
savedStateHandle: SavedStateHandle,
username: String,
toolId: String,
preview: Boolean,
userDataRepository: UserDataRepository,
storeRepository: StoreRepository,
toolRepository: ToolRepository,
storeDetailCache: MutableStateFlow<Result<ToolEntity>?>
@@ -83,31 +100,74 @@ private fun toolViewUiState(
val entityFlow =
if (!preview) toolRepository.getToolByUsernameAndToolId(username, toolId) else flowOf(null)
return flow {
combine(entityFlow, toolViewTemplate, ::Pair).collect { (entityFlow, toolViewTemplate) ->
if (entityFlow == null) {
savedStateHandle[IS_PREVIEW] = true
val cachedDetail = storeDetailCache.value
if (cachedDetail != null) {
emitResult(result = cachedDetail, toolViewTemplate = toolViewTemplate)
} else {
storeRepository.detail(username, toolId).collect { result ->
storeDetailCache.value = result
emitResult(result = result, toolViewTemplate = toolViewTemplate)
}
val isSystemDarkModeFlow = callbackFlow<Boolean> {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, instent: Intent?) {
context?.let(ResourcesUtils::getConfiguration)?.run {
trySend((uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES)
}
} else {
savedStateHandle[IS_PREVIEW] = false
emit(
ToolViewUiState.Success(
entityFlow.name,
processHtml(
toolViewTemplate = toolViewTemplate,
distBase64 = entityFlow.dist!!,
baseBase64 = entityFlow.base!!
)
)
)
}
}
val filter = IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)
context.registerReceiver(receiver, filter)
trySend((ResourcesUtils.getConfiguration(context).uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES)
awaitClose { context.unregisterReceiver(receiver) }
}
return isSystemDarkModeFlow.flatMapLatest { isSystemDarkMode ->
flow {
userDataRepository.userData.collect { userData ->
val isDarkMode: Boolean = when (userData.darkThemeConfig) {
DarkThemeConfig.FollowSystem -> isSystemDarkMode
DarkThemeConfig.Light -> false
DarkThemeConfig.Dark -> true
}
val globalJsVariables = toolRepository.getGlobalJsVariables(isDarkMode)
val globalCssVariables = toolRepository.getGlobalCssVariables(isDarkMode)
combine(entityFlow, toolViewTemplate, globalJsVariables, ::Triple)
.combine(globalCssVariables) { triple, globalCssVariables ->
Quadruple(triple.first, triple.second, triple.third, globalCssVariables)
}
.collect { (entityFlow, toolViewTemplate, globalJsVariables, globalCssVariables) ->
if (entityFlow == null) {
savedStateHandle[IS_PREVIEW] = true
val cachedDetail = storeDetailCache.value
if (cachedDetail != null) {
emitResult(
result = cachedDetail,
toolViewTemplate = toolViewTemplate,
globalJsVariables = globalJsVariables,
globalCssVariables = globalCssVariables
)
} else {
storeRepository.detail(username, toolId).collect { result ->
storeDetailCache.value = result
emitResult(
result = result,
toolViewTemplate = toolViewTemplate,
globalJsVariables = globalJsVariables,
globalCssVariables = globalCssVariables
)
}
}
} else {
savedStateHandle[IS_PREVIEW] = false
emit(
ToolViewUiState.Success(
entityFlow.name,
processHtml(
toolViewTemplate = toolViewTemplate,
globalJsVariables = globalJsVariables,
globalCssVariables = globalCssVariables,
distBase64 = entityFlow.dist!!,
baseBase64 = entityFlow.base!!
)
)
)
}
}
}
}
}
@@ -115,7 +175,9 @@ private fun toolViewUiState(
private suspend fun FlowCollector<ToolViewUiState>.emitResult(
result: Result<ToolEntity>,
toolViewTemplate: String
toolViewTemplate: String,
globalJsVariables: String,
globalCssVariables: String
) {
emit(
when (result) {
@@ -126,6 +188,8 @@ private suspend fun FlowCollector<ToolViewUiState>.emitResult(
result.data.name,
processHtml(
toolViewTemplate = toolViewTemplate,
globalJsVariables = globalJsVariables,
globalCssVariables = globalCssVariables,
distBase64 = dist,
baseBase64 = base
)
@@ -163,13 +227,23 @@ sealed interface WebViewInstanceState {
}
@OptIn(ExperimentalEncodingApi::class)
private fun processHtml(toolViewTemplate: String, distBase64: String, baseBase64: String): String {
private fun processHtml(
toolViewTemplate: String,
globalJsVariables: String,
globalCssVariables: String,
distBase64: String,
baseBase64: String
): String {
val dist = Base64.decodeToStringWithZip(distBase64)
val base = Base64.decodeToStringWithZip(baseBase64)
return toolViewTemplate
.replace(oldValue = "{{replace_global_js_variables}}", newValue = globalJsVariables)
.replace(oldValue = "{{replace_global_css_variables}}", newValue = globalCssVariables)
.replace(oldValue = "{{replace_dict_code}}", newValue = dist)
.replace(oldValue = "{{replace_base_code}}", newValue = base)
}
data class Quadruple<A, B, C, D>(val first: A, val second: B, val third: C, val fourth: D)
private const val IS_PREVIEW = "IS_PREVIEW"