Feat(Tool): Support theme
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -220,7 +220,8 @@ private fun Content(
|
||||
),
|
||||
factory = {
|
||||
webViewInstanceState.webView
|
||||
}
|
||||
},
|
||||
captureBackPresses = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user