First pass at label flair for iOS and Android
This commit is contained in:
@ -17,8 +17,8 @@ android {
|
||||
applicationId "app.omnivore.omnivore"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 110
|
||||
versionName "0.0.110"
|
||||
versionCode 118
|
||||
versionName "0.0.118"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -5,12 +5,12 @@ import app.omnivore.omnivore.graphql.generated.SetLabelsMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
|
||||
|
||||
suspend fun Networker.updateLabelsForSavedItem(input: SetLabelsInput): Boolean {
|
||||
suspend fun Networker.updateLabelsForSavedItem(input: SetLabelsInput): List<SetLabelsMutation.Label>? {
|
||||
return try {
|
||||
val result = authenticatedApolloClient().mutation(SetLabelsMutation(input)).execute()
|
||||
return result.data?.setLabels?.onSetLabelsSuccess?.labels != null
|
||||
return result.data?.setLabels?.onSetLabelsSuccess?.labels
|
||||
} catch (e: java.lang.Exception) {
|
||||
false
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -56,21 +56,6 @@ class LabelsViewModel @Inject constructor(
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
dataService.db.savedItemLabelDao().insertAll(listOf(res))
|
||||
|
||||
val newLabel = networker.createNewLabel(CreateLabelInput(color = presentIfNotNull(res.color), name = res.name))
|
||||
if (newLabel != null) {
|
||||
try {
|
||||
dataService.db.savedItemLabelDao().updateTempLabel(tempId, newLabel.id)
|
||||
} catch (e: Exception) {
|
||||
Log.d("EXCEPTION: ", e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,55 +304,44 @@ class LibraryViewModel @Inject constructor(
|
||||
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val syncedLabels = labels.filter { it.serverSyncStatus == ServerSyncStatus.IS_SYNCED.rawValue }
|
||||
val unsyncedLabels = labels.filter { it.serverSyncStatus != ServerSyncStatus.IS_SYNCED.rawValue }
|
||||
val input = SetLabelsInput(
|
||||
pageId = savedItemID,
|
||||
labels = Optional.presentIfNotNull(labels.map { CreateLabelInput(color = Optional.presentIfNotNull(it.color), name = it.name) }),
|
||||
)
|
||||
|
||||
var labelCreationError = false
|
||||
val createdLabels = unsyncedLabels.mapNotNull { label ->
|
||||
val result = networker.createNewLabel(CreateLabelInput(
|
||||
name = label.name,
|
||||
color = presentIfNotNull(label.color),
|
||||
description = presentIfNotNull(label.labelDescription),
|
||||
))
|
||||
result?.let {
|
||||
val updatedLabels = networker.updateLabelsForSavedItem(input)
|
||||
|
||||
// Figure out which of the labels are new
|
||||
updatedLabels?.let { updatedLabels ->
|
||||
val existingNamedLabels = dataService.db.savedItemLabelDao()
|
||||
.namedLabels(updatedLabels.map { it.labelFields.name })
|
||||
val existingNames = existingNamedLabels.map { it.name }
|
||||
val newNamedLabels = updatedLabels.filter { !existingNames.contains(it.labelFields.name) }
|
||||
|
||||
dataService.db.savedItemLabelDao().insertAll(newNamedLabels.map {
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = result.id,
|
||||
name = result.name,
|
||||
color = result.color,
|
||||
createdAt = result.createdAt.toString(),
|
||||
labelDescription = result.description,
|
||||
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
savedItemLabelId = it.labelFields.id,
|
||||
name = it.labelFields.name,
|
||||
color = it.labelFields.color,
|
||||
createdAt = null,
|
||||
labelDescription = null
|
||||
)
|
||||
})
|
||||
|
||||
val allNamedLabels = dataService.db.savedItemLabelDao()
|
||||
.namedLabels(updatedLabels.map { it.labelFields.name })
|
||||
val crossRefs = allNamedLabels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(
|
||||
savedItemLabelId = it.savedItemLabelId,
|
||||
savedItemId = savedItemID
|
||||
)
|
||||
} ?: run {
|
||||
labelCreationError = true
|
||||
null
|
||||
}
|
||||
}
|
||||
dataService.db.savedItemAndSavedItemLabelCrossRefDao().deleteRefsBySavedItemId(savedItemID)
|
||||
dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
|
||||
|
||||
dataService.db.savedItemLabelDao().insertAll(createdLabels)
|
||||
|
||||
val allLabels = syncedLabels + createdLabels
|
||||
|
||||
val input = SetLabelsInput(labelIds = Optional.presentIfNotNull(allLabels.map { it.savedItemLabelId }), pageId = savedItemID)
|
||||
val networkResult = networker.updateLabelsForSavedItem(input)
|
||||
|
||||
val crossRefs = allLabels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(
|
||||
savedItemLabelId = it.savedItemLabelId,
|
||||
savedItemId = savedItemID
|
||||
)
|
||||
}
|
||||
|
||||
// Remove all labels first
|
||||
dataService.db.savedItemAndSavedItemLabelCrossRefDao().deleteRefsBySavedItemId(savedItemID)
|
||||
|
||||
// Add back the current labels
|
||||
dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
|
||||
|
||||
if (!networkResult || labelCreationError) {
|
||||
snackbarMessage = resourceProvider.getString(R.string.library_view_model_snackbar_error)
|
||||
} else {
|
||||
snackbarMessage = resourceProvider.getString(R.string.library_view_model_snackbar_success)
|
||||
} ?: run {
|
||||
snackbarMessage = resourceProvider.getString(R.string.library_view_model_snackbar_error)
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
|
||||
@ -20,16 +20,20 @@ import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.compose.ui.text.toUpperCase
|
||||
import androidx.compose.ui.unit.*
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.ui.components.LabelChipColors
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import app.omnivore.omnivore.ui.library.SavedItemFilter
|
||||
import app.omnivore.omnivore.ui.library.SavedItemViewModel
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
|
||||
@ -45,14 +49,14 @@ fun SavedItemCard(
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClickHandler,
|
||||
onLongClick = {
|
||||
savedItemViewModel.actionsMenuItemLiveData.postValue(savedItem)
|
||||
}
|
||||
)
|
||||
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.background)
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = onClickHandler,
|
||||
onLongClick = {
|
||||
savedItemViewModel.actionsMenuItemLiveData.postValue(savedItem)
|
||||
}
|
||||
)
|
||||
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.background)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@ -108,8 +112,10 @@ fun SavedItemCard(
|
||||
)
|
||||
}
|
||||
|
||||
FlowRow(modifier = Modifier.fillMaxWidth().padding(10.dp)) {
|
||||
savedItem.labels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) }).forEach { label ->
|
||||
FlowRow(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)) {
|
||||
savedItem.labels.filter { !isFlairLabel(it) }.sortedWith(compareBy { it.name.toLowerCase(Locale.current) }).forEach { label ->
|
||||
val chipColors = LabelChipColors.fromHex(label.color)
|
||||
|
||||
LabelChip(
|
||||
@ -196,6 +202,82 @@ fun readingProgress(item: SavedItemWithLabelsAndHighlights): String {
|
||||
// return ""
|
||||
//}
|
||||
|
||||
|
||||
public enum class FlairIcon(
|
||||
public val rawValue: String,
|
||||
public val sortOrder: Int
|
||||
) {
|
||||
FEED("feed", 0),
|
||||
RSS("rss", 0),
|
||||
FAVORITE("favorite", 1),
|
||||
NEWSLETTER("newsletter", 2),
|
||||
RECOMMENDED("recommended", 3),
|
||||
PINNED("pinned", 4)
|
||||
}
|
||||
|
||||
val FLAIR_ICON_NAMES = listOf("feed", "rss", "favorite", "newsletter", "recommended", "pinned")
|
||||
|
||||
fun isFlairLabel(label: SavedItemLabel): Boolean {
|
||||
return FLAIR_ICON_NAMES.contains(label.name.toLowerCase(Locale.current))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun flairIcons(item: SavedItemWithLabelsAndHighlights) {
|
||||
val labels = item.labels.filter { isFlairLabel(it) }.map {
|
||||
FlairIcon.valueOf(it.name.toUpperCase(Locale.current))
|
||||
}
|
||||
labels.forEach {
|
||||
when (it) {
|
||||
FlairIcon.RSS,
|
||||
FlairIcon.FEED -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flair_feed),
|
||||
contentDescription = "Feed flair Icon",
|
||||
modifier = Modifier
|
||||
.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
FlairIcon.FAVORITE -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flaire_favorite),
|
||||
contentDescription = "Favorite flair Icon",
|
||||
modifier = Modifier
|
||||
.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
FlairIcon.NEWSLETTER -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flair_newsletter),
|
||||
contentDescription = "Newsletter flair Icon",
|
||||
modifier = Modifier
|
||||
.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
FlairIcon.RECOMMENDED -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flair_recommended),
|
||||
contentDescription = "Recommended flair Icon",
|
||||
modifier = Modifier
|
||||
.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
FlairIcon.PINNED -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flair_pinned),
|
||||
contentDescription = "Pinned flair Icon",
|
||||
modifier = Modifier
|
||||
.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun readInfo(item: SavedItemWithLabelsAndHighlights) {
|
||||
Row(
|
||||
@ -203,6 +285,9 @@ fun readInfo(item: SavedItemWithLabelsAndHighlights) {
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 15.dp)
|
||||
) {
|
||||
// Show flair here
|
||||
flairIcons(item)
|
||||
|
||||
Text(
|
||||
text = estimatedReadingTime(item),
|
||||
style = TextStyle(
|
||||
|
||||
31
android/Omnivore/app/src/main/res/drawable/flair_feed.xml
Normal file
31
android/Omnivore/app/src/main/res/drawable/flair_feed.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17dp"
|
||||
android:height="17dp"
|
||||
android:viewportWidth="17"
|
||||
android:viewportHeight="17">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0.739,0.566h16v16h-16z"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M8.739,3.232L3.405,5.899L8.739,8.566L14.072,5.899L8.739,3.232Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#FF7B03"
|
||||
android:strokeColor="#FF7B03"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M3.405,8.566L8.739,11.233L14.072,8.566"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FF7B03"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M3.405,11.232L8.739,13.899L14.072,11.232"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FF7B03"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
||||
@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17dp"
|
||||
android:height="17dp"
|
||||
android:viewportWidth="17"
|
||||
android:viewportHeight="17">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0.739,0.566h16v16h-16z"/>
|
||||
<path
|
||||
android:pathData="M15.406,5.59V11.9C15.406,12.41 15.211,12.901 14.861,13.272C14.511,13.643 14.032,13.867 13.523,13.896L13.406,13.9H4.072C3.562,13.9 3.071,13.705 2.7,13.355C2.329,13.005 2.106,12.526 2.076,12.017L2.072,11.9V5.59L8.369,9.788L8.446,9.832C8.537,9.876 8.637,9.9 8.739,9.9C8.84,9.9 8.94,9.876 9.032,9.832L9.109,9.788L15.406,5.59Z"
|
||||
android:fillColor="#007AFF"/>
|
||||
<path
|
||||
android:pathData="M13.405,3.232C14.125,3.232 14.757,3.612 15.109,4.184L8.739,8.43L2.369,4.184C2.536,3.912 2.765,3.685 3.038,3.52C3.311,3.355 3.62,3.258 3.938,3.237L4.072,3.232H13.405Z"
|
||||
android:fillColor="#007AFF"/>
|
||||
</group>
|
||||
</vector>
|
||||
13
android/Omnivore/app/src/main/res/drawable/flair_pinned.xml
Normal file
13
android/Omnivore/app/src/main/res/drawable/flair_pinned.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17dp"
|
||||
android:height="17dp"
|
||||
android:viewportWidth="17"
|
||||
android:viewportHeight="17">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0.739,0.566h16v16h-16z"/>
|
||||
<path
|
||||
android:pathData="M10.814,2.706L10.877,2.762L14.543,6.428C14.656,6.541 14.724,6.691 14.736,6.85C14.747,7.009 14.702,7.166 14.607,7.295C14.512,7.423 14.375,7.513 14.219,7.548C14.064,7.584 13.901,7.562 13.76,7.488L11.645,9.602L10.696,12.134C10.671,12.2 10.635,12.263 10.591,12.318L10.544,12.372L9.544,13.372C9.429,13.486 9.276,13.555 9.114,13.565C8.952,13.575 8.792,13.526 8.664,13.426L8.601,13.371L6.739,11.509L4.21,14.038C4.09,14.157 3.929,14.226 3.76,14.232C3.59,14.237 3.426,14.177 3.299,14.065C3.171,13.953 3.092,13.797 3.076,13.629C3.06,13.46 3.108,13.292 3.212,13.158L3.267,13.095L5.795,10.566L3.934,8.704C3.819,8.589 3.75,8.437 3.74,8.275C3.73,8.113 3.779,7.952 3.879,7.824L3.934,7.762L4.934,6.762C4.984,6.711 5.042,6.669 5.106,6.637L5.171,6.609L7.702,5.659L9.816,3.546C9.744,3.411 9.72,3.255 9.749,3.105C9.778,2.955 9.858,2.819 9.975,2.721C10.092,2.623 10.239,2.567 10.392,2.565C10.544,2.562 10.693,2.612 10.814,2.706Z"
|
||||
android:fillColor="#3D3D3D"/>
|
||||
</group>
|
||||
</vector>
|
||||
@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17dp"
|
||||
android:height="17dp"
|
||||
android:viewportWidth="17"
|
||||
android:viewportHeight="17">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0.739,0.566h16v16h-16z"/>
|
||||
<path
|
||||
android:pathData="M9.406,2.566C9.916,2.566 10.407,2.761 10.778,3.111C11.149,3.461 11.372,3.94 11.402,4.449L11.406,4.566V7.233H12.739C13.229,7.233 13.702,7.413 14.068,7.739C14.434,8.064 14.668,8.513 14.726,9L14.736,9.116L14.739,9.233L14.726,9.364L14.055,12.718C13.801,13.802 13.054,14.582 12.182,14.572L12.072,14.566H6.739C6.576,14.566 6.418,14.506 6.296,14.398C6.174,14.289 6.096,14.14 6.077,13.978L6.072,13.9L6.073,7.542C6.073,7.425 6.104,7.311 6.162,7.209C6.221,7.108 6.305,7.024 6.406,6.966C6.691,6.802 6.93,6.57 7.103,6.291C7.277,6.012 7.379,5.695 7.401,5.368L7.406,5.233V4.566C7.406,4.036 7.616,3.527 7.991,3.152C8.366,2.777 8.875,2.566 9.406,2.566Z"
|
||||
android:fillColor="#FEC43F"/>
|
||||
<path
|
||||
android:pathData="M4.072,7.232C4.236,7.232 4.393,7.292 4.515,7.401C4.637,7.509 4.715,7.659 4.734,7.821L4.739,7.899V13.899C4.739,14.062 4.679,14.22 4.57,14.342C4.462,14.464 4.312,14.542 4.15,14.561L4.072,14.566H3.406C3.069,14.566 2.745,14.439 2.499,14.21C2.252,13.981 2.101,13.668 2.076,13.332L2.072,13.232V8.566C2.072,8.229 2.199,7.905 2.428,7.659C2.657,7.412 2.97,7.261 3.306,7.236L3.406,7.232H4.072Z"
|
||||
android:fillColor="#FEC43F"/>
|
||||
</group>
|
||||
</vector>
|
||||
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17dp"
|
||||
android:height="17dp"
|
||||
android:viewportWidth="17"
|
||||
android:viewportHeight="17">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0.739,0.566h16v16h-16z"/>
|
||||
<path
|
||||
android:pathData="M5.391,2.615C5.981,2.515 6.586,2.548 7.162,2.712C7.738,2.877 8.269,3.168 8.717,3.565L8.741,3.587L8.764,3.567C9.191,3.192 9.694,2.913 10.238,2.747C10.782,2.582 11.355,2.534 11.919,2.607L12.083,2.631C12.794,2.754 13.459,3.067 14.006,3.536C14.554,4.006 14.965,4.615 15.194,5.299C15.424,5.982 15.465,6.716 15.312,7.421C15.159,8.126 14.818,8.776 14.326,9.303L14.206,9.427L14.174,9.454L9.207,14.373C9.093,14.487 8.941,14.555 8.78,14.565C8.619,14.575 8.46,14.526 8.332,14.428L8.269,14.373L3.274,9.425C2.745,8.911 2.368,8.259 2.187,7.544C2.005,6.828 2.025,6.076 2.244,5.371C2.463,4.666 2.873,4.035 3.429,3.549C3.984,3.063 4.664,2.739 5.391,2.615Z"
|
||||
android:fillColor="#F8023B"/>
|
||||
</group>
|
||||
</vector>
|
||||
Reference in New Issue
Block a user