Merge pull request #2123 from omnivore-app/fix/android-theme

Use Black as the themeKey for system dark mode on Android, refresh content less frequently, add Justify Text option
This commit is contained in:
Jackson Harper
2023-04-28 10:02:17 +08:00
committed by GitHub
12 changed files with 223 additions and 121 deletions

View File

@ -17,8 +17,8 @@ android {
applicationId "app.omnivore.omnivore"
minSdk 26
targetSdk 33
versionCode 47
versionName "0.0.47"
versionCode 50
versionName "0.0.50"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -15,6 +15,7 @@ object DatastoreKeys {
const val preferredWebMaxWidthPercentage = "preferredWebMaxWidthPercentage"
const val preferredWebFontFamily = "preferredWebFontFamily"
const val prefersWebHighContrastText = "prefersWebHighContrastText"
const val prefersJustifyText = "prefersJustifyText"
const val lastUsedSavedItemFilter = "lastUsedSavedItemFilter"
const val lastUsedSavedItemSortFilter = "lastUsedSavedItemSortFilter"
const val preferredTheme = "preferredTheme"

View File

@ -0,0 +1,33 @@
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import app.omnivore.omnivore.ui.components.LabelChipColors
@Composable
fun LabelChip(
name: String,
colors: LabelChipColors,
onSelectionChanged: (String) -> Unit = {},
) {
Surface(
modifier = Modifier.padding(4.dp),
shape = MaterialTheme.shapes.medium,
color = colors.containerColor
) {
Row(modifier = Modifier
) {
Text(
text = name,
color = colors.textColor,
style = MaterialTheme.typography.subtitle2,
modifier = Modifier.padding(vertical = 3.dp, horizontal = 5.dp)
)
}
}
}

View File

@ -1,20 +1,31 @@
@file:OptIn(ExperimentalMaterialApi::class)
package app.omnivore.omnivore.ui.components
import LabelChip
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@ -29,28 +40,37 @@ fun WebReaderLabelsSelectionSheet(viewModel: WebReaderViewModel) {
val labels: List<SavedItemLabel> by viewModel.savedItemLabelsLiveData.observeAsState(listOf())
val webReaderParams: WebReaderParams? by viewModel.webReaderParamsLiveData.observeAsState(null)
val modalBottomSheetState = rememberModalBottomSheetState(
ModalBottomSheetValue.HalfExpanded,
confirmStateChange = { it != ModalBottomSheetValue.Hidden }
)
if (isActive) {
Dialog(onDismissRequest = {
viewModel.showLabelsSelectionSheetLiveData.value = false
} ) {
LabelsSelectionSheetContent(
labels = labels,
initialSelectedLabels = webReaderParams?.labels ?: listOf(),
onCancel = {
viewModel.showLabelsSelectionSheetLiveData.value = false
},
isLibraryMode = false,
onSave = {
if (it != labels) {
viewModel.updateSavedItemLabels(savedItemID = webReaderParams?.item?.savedItemId ?: "", labels = it)
}
viewModel.showLabelsSelectionSheetLiveData.value = false
},
onCreateLabel = { newLabelName, labelHexValue ->
viewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
ModalBottomSheetLayout(
sheetBackgroundColor = Color.Transparent,
sheetState = modalBottomSheetState,
sheetContent = {
BottomSheetUI {
LabelsSelectionSheetContent(
labels = labels,
initialSelectedLabels = webReaderParams?.labels ?: listOf(),
onCancel = {
viewModel.showLabelsSelectionSheetLiveData.value = false
},
isLibraryMode = false,
onSave = {
if (it != labels) {
viewModel.updateSavedItemLabels(savedItemID = webReaderParams?.item?.savedItemId ?: "", labels = it)
}
viewModel.showLabelsSelectionSheetLiveData.value = false
},
onCreateLabel = { newLabelName, labelHexValue ->
viewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
}
)
}
)
}
}
) {}
}
}
@ -60,48 +80,59 @@ fun LabelsSelectionSheet(viewModel: LibraryViewModel) {
val labels: List<SavedItemLabel> by viewModel.savedItemLabelsLiveData.observeAsState(listOf())
val currentSavedItemData = viewModel.currentSavedItemUnderEdit()
val modalBottomSheetState = rememberModalBottomSheetState(
ModalBottomSheetValue.HalfExpanded,
confirmStateChange = { it != ModalBottomSheetValue.Hidden }
)
if (isActive) {
Dialog(onDismissRequest = {
viewModel.labelsSelectionCurrentItemLiveData.value = null
viewModel.showLabelsSelectionSheetLiveData.value = false
} ) {
if (currentSavedItemData != null) {
LabelsSelectionSheetContent(
labels = labels,
initialSelectedLabels = currentSavedItemData.labels,
onCancel = {
viewModel.showLabelsSelectionSheetLiveData.value = false
viewModel.labelsSelectionCurrentItemLiveData.value = null
},
isLibraryMode = false,
onSave = {
if (it != labels) {
viewModel.updateSavedItemLabels(savedItemID = currentSavedItemData.savedItem.savedItemId, labels = it)
}
viewModel.labelsSelectionCurrentItemLiveData.value = null
viewModel.showLabelsSelectionSheetLiveData.value = false
},
onCreateLabel = { newLabelName, labelHexValue ->
viewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
ModalBottomSheetLayout(
sheetBackgroundColor = Color.Transparent,
sheetState = modalBottomSheetState,
sheetContent = {
BottomSheetUI {
if (currentSavedItemData != null) {
LabelsSelectionSheetContent(
labels = labels,
initialSelectedLabels = currentSavedItemData.labels,
onCancel = {
viewModel.showLabelsSelectionSheetLiveData.value = false
viewModel.labelsSelectionCurrentItemLiveData.value = null
},
isLibraryMode = false,
onSave = {
if (it != labels) {
viewModel.updateSavedItemLabels(
savedItemID = currentSavedItemData.savedItem.savedItemId,
labels = it
)
}
viewModel.labelsSelectionCurrentItemLiveData.value = null
viewModel.showLabelsSelectionSheetLiveData.value = false
},
onCreateLabel = { newLabelName, labelHexValue ->
viewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
}
)
} else { // Is used in library mode
LabelsSelectionSheetContent(
labels = labels,
initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf(),
onCancel = { viewModel.showLabelsSelectionSheetLiveData.value = false },
isLibraryMode = true,
onSave = {
viewModel.updateAppliedLabels(it)
viewModel.labelsSelectionCurrentItemLiveData.value = null
viewModel.showLabelsSelectionSheetLiveData.value = false
},
onCreateLabel = { newLabelName, labelHexValue ->
viewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
}
)
}
)
} else { // Is used in library mode
LabelsSelectionSheetContent(
labels = labels,
initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf(),
onCancel = { viewModel.showLabelsSelectionSheetLiveData.value = false },
isLibraryMode = true,
onSave = {
viewModel.updateAppliedLabels(it)
viewModel.labelsSelectionCurrentItemLiveData.value = null
viewModel.showLabelsSelectionSheetLiveData.value = false
},
onCreateLabel = { newLabelName, labelHexValue ->
viewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
}
)
}
}
}
) {}
}
}
@ -125,7 +156,6 @@ fun LabelsSelectionSheetContent(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
shape = RoundedCornerShape(16.dp)
) {
if (showCreateLabelDialog) {
@ -142,8 +172,9 @@ fun LabelsSelectionSheetContent(
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
// .verticalScroll(rememberScrollState())
.fillMaxSize()
.padding(horizontal = 6.dp)
.padding(horizontal = 0.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
@ -158,7 +189,7 @@ fun LabelsSelectionSheetContent(
Text(titleText, fontWeight = FontWeight.ExtraBold)
TextButton(onClick = { onSave(selectedLabels.value) }) {
Text(text = "Done")
Text(text = "Save")
}
}
LazyColumn(
@ -184,19 +215,13 @@ fun LabelsSelectionSheetContent(
selectedLabels.value = selectedLabels.value + listOf(label)
}
}
.padding(horizontal = 6.dp)
.padding(horizontal = 10.dp, vertical = 6.dp)
) {
val chipColors = LabelChipColors.fromHex(label.color)
SuggestionChip(
onClick = {},
label = { Text(label.name) },
border = null,
colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
containerColor = chipColors.containerColor,
labelColor = chipColors.textColor,
iconContentColor = chipColors.textColor
)
LabelChip(
name = label.name,
colors = chipColors
)
if (isLabelSelected) {
Icon(
@ -233,3 +258,17 @@ fun LabelsSelectionSheetContent(
}
}
}
@Composable
private fun BottomSheetUI(content: @Composable () -> Unit) {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp))
.background(Color.White)
.statusBarsPadding()
) {
content()
}
}

View File

@ -49,6 +49,9 @@ fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) {
val currentWebPreferences = webReaderViewModel.storedWebPreferences(isDark)
val isFontListExpanded = remember { mutableStateOf(false) }
val highContrastTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersHighContrastText) }
val justifyTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersJustifyText) }
val selectedWebFontRawValue = remember { mutableStateOf(currentWebPreferences.fontFamily.rawValue) }
Column(
@ -98,6 +101,18 @@ fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) {
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Justify Text")
Spacer(modifier = Modifier.weight(1.0F))
Switch(
checked = justifyTextSwitchState.value,
onCheckedChange = {
justifyTextSwitchState.value = it
webReaderViewModel.updateJustifyText(it)
}
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@ -200,5 +215,6 @@ data class WebPreferences(
val themeKey: String,
val storedThemePreference: String,
val fontFamily: WebFont,
val prefersHighContrastText: Boolean
val prefersHighContrastText: Boolean,
val prefersJustifyText: Boolean
)

View File

@ -30,8 +30,8 @@ import java.util.*
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebReader(
params: WebReaderParams,
preferences: WebPreferences,
styledContent: String,
webReaderViewModel: WebReaderViewModel
) {
val javascriptActionLoopUUID: UUID by webReaderViewModel
@ -40,23 +40,9 @@ fun WebReader(
WebView.setWebContentsDebuggingEnabled(true)
val webReaderContent = WebReaderContent(
preferences = preferences,
item = params.item,
articleContent = params.articleContent,
)
val styledContent = webReaderContent.styledContent()
val isInDarkMode = preferences.themeKey == "Dark"
Box {
AndroidView(factory = {
OmnivoreWebView(it).apply {
if (isInDarkMode) {
setBackgroundColor(Color.Transparent.hashCode())
} else {
setBackgroundColor(Color.White.hashCode())
}
viewModel = webReaderViewModel
layoutParams = ViewGroup.LayoutParams(

View File

@ -43,11 +43,11 @@ data class WebReaderContent(
val articleContent: ArticleContent,
) {
fun styledContent(): String {
// TODO: Kotlinize these three values (pasted from Swift)
val savedAt = "new Date(1662571290735.0).toISOString()"
val createdAt = "new Date().toISOString()"
val publishedAt =
"new Date().toISOString()" //if (item.publishDate != null) "new Date((item.publishDate!.timeIntervalSince1970 * 1000)).toISOString()" else "undefined"
val savedAt = "\"${item.savedAt}\""
val createdAt = "\"${item.createdAt}\""
val publishedAt = if (item.publishDate != null) "\"${item.publishDate}\"" else "undefined"
val textFontSize = preferences.textFontSize
val highlightCssFilePath = "highlight${if (preferences.themeKey == "Dark") "-dark" else ""}.css"
@ -80,9 +80,9 @@ data class WebReaderContent(
id: "${item.savedItemId}",
linkId: "${item.savedItemId}",
slug: "${item.slug}",
createdAt: new Date(1662571290735.0).toISOString(),
savedAt: new Date(1662571290981.0).toISOString(),
publishedAt: new Date(1662454816000.0).toISOString(),
createdAt: ${createdAt},
savedAt: ${savedAt},
publishedAt: ${publishedAt},
url: `${item.pageURLString}`,
title: `${articleContent.title.replace("`", "\\`")}`,
content: document.getElementById('_omnivore-htmlContent').innerHTML,
@ -100,6 +100,7 @@ data class WebReaderContent(
window.maxWidthPercentage = ${preferences.maxWidthPercentage}
window.lineHeight = ${preferences.lineHeight}
window.prefersHighContrastFont = ${preferences.prefersHighContrastText}
window.justifyText = ${preferences.prefersJustifyText}
window.enableHighlightBar = false
</script>
<script src="bundle.js"></script>

View File

@ -122,17 +122,26 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, o
webReaderViewModel.maxToolbarHeightPx = with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() }
webReaderViewModel.loadItem(slug = slug, requestID = requestID)
val styledContent = webReaderParams?.let {
val webReaderContent = WebReaderContent(
preferences = webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()),
item = it.item,
articleContent = it.articleContent,
)
webReaderContent.styledContent()
} ?: null
Box(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.background(color = backgroundColor)
) {
if (webReaderParams != null) {
if (styledContent != null) {
WebReader(
webReaderParams!!,
webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()),
webReaderViewModel
preferences = webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()),
styledContent = styledContent,
webReaderViewModel = webReaderViewModel
)
TopAppBar(

View File

@ -56,7 +56,8 @@ class WebReaderViewModel @Inject constructor(
val showLabelsSelectionSheetLiveData = MutableLiveData(false)
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
val systemThemeKeys = listOf("Light", "Dark", "System")
// "Sepia", "Apollo",
val systemThemeKeys = listOf("Light", "Black", "System")
var hasTappedExistingHighlight = false
var lastTapCoordinates: TapCoordinates? = null
@ -265,6 +266,7 @@ class WebReaderViewModel @Inject constructor(
val storedWebFont = WebFont.values().first { it.rawValue == storedFontFamily }
val prefersHighContrastFont = datastoreRepo.getString(DatastoreKeys.prefersWebHighContrastText) == "true"
val prefersJustifyText = datastoreRepo.getString(DatastoreKeys.prefersJustifyText) == "true"
WebPreferences(
textFontSize = storedFontSize ?: 12,
@ -273,13 +275,14 @@ class WebReaderViewModel @Inject constructor(
themeKey = themeKey(isDarkMode, storedThemePreference),
storedThemePreference = storedThemePreference,
fontFamily = storedWebFont,
prefersHighContrastText = prefersHighContrastFont
prefersHighContrastText = prefersHighContrastFont,
prefersJustifyText = prefersJustifyText
)
}
fun themeKey(isDarkMode: Boolean, storedThemePreference: String): String {
if (storedThemePreference == "System") {
return if (isDarkMode) "Dark" else "Light"
return if (isDarkMode) "Black" else "Light"
}
return storedThemePreference
@ -292,7 +295,7 @@ class WebReaderViewModel @Inject constructor(
datastoreRepo.putString(DatastoreKeys.preferredTheme, systemThemeKeys[index])
}
val isDark = newThemeKey == "Dark"
val isDark = newThemeKey == "Dark" || newThemeKey == "Black"
val script = "var event = new Event('updateColorMode');event.isDark = '$isDark';document.dispatchEvent(event);"
enqueueScript(script)
}
@ -351,6 +354,14 @@ class WebReaderViewModel @Inject constructor(
enqueueScript(script)
}
fun updateJustifyText(justifyText: Boolean) {
runBlocking {
datastoreRepo.putString(DatastoreKeys.prefersJustifyText, justifyText.toString())
}
val script = "var event = new Event('updateJustifyText');event.justifyText = $justifyText;document.dispatchEvent(event);"
enqueueScript(script)
}
fun applyWebFont(font: WebFont) {
runBlocking {
datastoreRepo.putString(DatastoreKeys.preferredWebFontFamily, font.rawValue)

View File

@ -39,12 +39,11 @@ abstract class SaveSheetActivityBase: AppCompatActivity() {
Intent.ACTION_SEND -> {
if (intent.type?.startsWith("text/plain") == true) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
Log.d(ContentValues.TAG, "Extracted text: $extractedText")
extractedText = it
viewModel.saveURL(it)
Log.d(ContentValues.TAG, "Extracted text: $extractedText")
}
}
if (intent.type?.startsWith("text/html") == true) {
intent.getStringExtra(Intent.EXTRA_HTML_TEXT)?.let {
extractedText = it
@ -155,7 +154,9 @@ abstract class SaveSheetActivityBase: AppCompatActivity() {
viewModel: SaveViewModel,
modalBottomSheetState: ModalBottomSheetState
) {
Box(modifier = Modifier.height(300.dp).background(Color.White)) {
Box(modifier = Modifier
.height(300.dp)
.background(Color.White)) {
SaveContent(viewModel, modalBottomSheetState, modifier = Modifier.fillMaxSize())
}
}

View File

@ -17,6 +17,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.*
import java.util.regex.Pattern
import javax.inject.Inject
@HiltViewModel
@ -36,6 +37,11 @@ class SaveViewModel @Inject constructor(
datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken)
}
fun cleanUrl(url: String): String? {
return Regex("https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)")
.findAll(url).map { it.value }.first()
}
fun saveURL(url: String) {
viewModelScope.launch {
isLoading = true
@ -54,6 +60,9 @@ class SaveViewModel @Inject constructor(
.addHttpHeader("Authorization", value = authToken)
.build()
// Attempt to parse the URL out of the text, if that fails send the text
val cleanedUrl = cleanUrl(url) ?: url
try {
clientRequestID = UUID.randomUUID().toString()
@ -62,7 +71,7 @@ class SaveViewModel @Inject constructor(
SaveUrlInput(
clientRequestId = clientRequestID!!,
source = "android",
url = url
url = cleanedUrl
)
)
).execute()

View File

@ -1,5 +1,6 @@
package app.omnivore.omnivore.ui.savedItemViews
import LabelChip
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
@ -102,21 +103,16 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWi
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 10.dp, bottom = 5.dp, end = 10.dp)
.padding(start = 5.dp, bottom = 5.dp, end = 10.dp)
) {
items(savedItem.labels.sortedBy { it.name }) { label ->
val chipColors = LabelChipColors.fromHex(label.color)
SuggestionChip(
onClick = onClickHandler,
label = { Text(label.name) },
border = null,
colors = elevatedSuggestionChipColors(
containerColor = chipColors.containerColor,
labelColor = chipColors.textColor,
iconContentColor = chipColors.textColor
),
modifier = Modifier.padding(end = 5.dp)
LabelChip(
// onClick = onClickHandler,
name = label.name,
colors = chipColors,
// modifier = Modifier.padding(end = 5.dp)
)
}