Merge pull request #2981 from omnivore-app/feat/label-flair
First pass at label flair for iOS and Android
@ -17,8 +17,8 @@ android {
|
|||||||
applicationId "app.omnivore.omnivore"
|
applicationId "app.omnivore.omnivore"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 110
|
versionCode 118
|
||||||
versionName "0.0.110"
|
versionName "0.0.118"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
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.CreateLabelInput
|
||||||
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
|
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 {
|
return try {
|
||||||
val result = authenticatedApolloClient().mutation(SetLabelsMutation(input)).execute()
|
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) {
|
} catch (e: java.lang.Exception) {
|
||||||
false
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,21 +56,6 @@ class LabelsViewModel @Inject constructor(
|
|||||||
serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
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
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -304,55 +304,44 @@ class LibraryViewModel @Inject constructor(
|
|||||||
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
|
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val syncedLabels = labels.filter { it.serverSyncStatus == ServerSyncStatus.IS_SYNCED.rawValue }
|
val input = SetLabelsInput(
|
||||||
val unsyncedLabels = labels.filter { it.serverSyncStatus != ServerSyncStatus.IS_SYNCED.rawValue }
|
pageId = savedItemID,
|
||||||
|
labels = Optional.presentIfNotNull(labels.map { CreateLabelInput(color = Optional.presentIfNotNull(it.color), name = it.name) }),
|
||||||
|
)
|
||||||
|
|
||||||
var labelCreationError = false
|
val updatedLabels = networker.updateLabelsForSavedItem(input)
|
||||||
val createdLabels = unsyncedLabels.mapNotNull { label ->
|
|
||||||
val result = networker.createNewLabel(CreateLabelInput(
|
// Figure out which of the labels are new
|
||||||
name = label.name,
|
updatedLabels?.let { updatedLabels ->
|
||||||
color = presentIfNotNull(label.color),
|
val existingNamedLabels = dataService.db.savedItemLabelDao()
|
||||||
description = presentIfNotNull(label.labelDescription),
|
.namedLabels(updatedLabels.map { it.labelFields.name })
|
||||||
))
|
val existingNames = existingNamedLabels.map { it.name }
|
||||||
result?.let {
|
val newNamedLabels = updatedLabels.filter { !existingNames.contains(it.labelFields.name) }
|
||||||
|
|
||||||
|
dataService.db.savedItemLabelDao().insertAll(newNamedLabels.map {
|
||||||
SavedItemLabel(
|
SavedItemLabel(
|
||||||
savedItemLabelId = result.id,
|
savedItemLabelId = it.labelFields.id,
|
||||||
name = result.name,
|
name = it.labelFields.name,
|
||||||
color = result.color,
|
color = it.labelFields.color,
|
||||||
createdAt = result.createdAt.toString(),
|
createdAt = null,
|
||||||
labelDescription = result.description,
|
labelDescription = null
|
||||||
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
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 {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
|||||||
@ -20,16 +20,20 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.text.toLowerCase
|
import androidx.compose.ui.text.toLowerCase
|
||||||
|
import androidx.compose.ui.text.toUpperCase
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import app.omnivore.omnivore.R
|
import app.omnivore.omnivore.R
|
||||||
|
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||||
import app.omnivore.omnivore.ui.components.LabelChipColors
|
import app.omnivore.omnivore.ui.components.LabelChipColors
|
||||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||||
|
import app.omnivore.omnivore.ui.library.SavedItemFilter
|
||||||
import app.omnivore.omnivore.ui.library.SavedItemViewModel
|
import app.omnivore.omnivore.ui.library.SavedItemViewModel
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
|
||||||
@ -45,14 +49,14 @@ fun SavedItemCard(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onClickHandler,
|
onClick = onClickHandler,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
savedItemViewModel.actionsMenuItemLiveData.postValue(savedItem)
|
savedItemViewModel.actionsMenuItemLiveData.postValue(savedItem)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.background)
|
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.background)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@ -108,8 +112,10 @@ fun SavedItemCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FlowRow(modifier = Modifier.fillMaxWidth().padding(10.dp)) {
|
FlowRow(modifier = Modifier
|
||||||
savedItem.labels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) }).forEach { label ->
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp)) {
|
||||||
|
savedItem.labels.filter { !isFlairLabel(it) }.sortedWith(compareBy { it.name.toLowerCase(Locale.current) }).forEach { label ->
|
||||||
val chipColors = LabelChipColors.fromHex(label.color)
|
val chipColors = LabelChipColors.fromHex(label.color)
|
||||||
|
|
||||||
LabelChip(
|
LabelChip(
|
||||||
@ -196,6 +202,82 @@ fun readingProgress(item: SavedItemWithLabelsAndHighlights): String {
|
|||||||
// return ""
|
// 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
|
@Composable
|
||||||
fun readInfo(item: SavedItemWithLabelsAndHighlights) {
|
fun readInfo(item: SavedItemWithLabelsAndHighlights) {
|
||||||
Row(
|
Row(
|
||||||
@ -203,6 +285,9 @@ fun readInfo(item: SavedItemWithLabelsAndHighlights) {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.defaultMinSize(minHeight = 15.dp)
|
.defaultMinSize(minHeight = 15.dp)
|
||||||
) {
|
) {
|
||||||
|
// Show flair here
|
||||||
|
flairIcons(item)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = estimatedReadingTime(item),
|
text = estimatedReadingTime(item),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
|||||||
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
@ -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>
|
||||||
@ -1400,7 +1400,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
MARKETING_VERSION = 1.34.0;
|
MARKETING_VERSION = 1.35.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
||||||
@ -1435,7 +1435,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
MARKETING_VERSION = 1.34.0;
|
MARKETING_VERSION = 1.35.0;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@ -1490,7 +1490,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.34.0;
|
MARKETING_VERSION = 1.35.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
||||||
PRODUCT_NAME = Omnivore;
|
PRODUCT_NAME = Omnivore;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -1831,7 +1831,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.34.0;
|
MARKETING_VERSION = 1.35.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
||||||
PRODUCT_NAME = Omnivore;
|
PRODUCT_NAME = Omnivore;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@ -2,6 +2,35 @@ import Models
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
|
enum FlairLabels: String {
|
||||||
|
case pinned
|
||||||
|
case favorite
|
||||||
|
case recommended
|
||||||
|
case newsletter
|
||||||
|
case rss
|
||||||
|
case feed
|
||||||
|
|
||||||
|
var icon: Image {
|
||||||
|
switch self {
|
||||||
|
case .pinned: return Image.flairPinned
|
||||||
|
case .favorite: return Image.flairFavorite
|
||||||
|
case .recommended: return Image.flairRecommended
|
||||||
|
case .newsletter: return Image.flairNewsletter
|
||||||
|
case .feed, .rss: return Image.flairFeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortOrder: Int {
|
||||||
|
switch self {
|
||||||
|
case .feed, .rss: return 0
|
||||||
|
case .favorite: return 1
|
||||||
|
case .newsletter: return 2
|
||||||
|
case .recommended: return 3
|
||||||
|
case .pinned: return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
func draggableItem(item: LinkedItem) -> some View {
|
func draggableItem(item: LinkedItem) -> some View {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@ -124,8 +153,30 @@ public struct LibraryItemCard: View {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var flairLabels: [FlairLabels] {
|
||||||
|
item.sortedLabels.compactMap { label in
|
||||||
|
if let name = label.name {
|
||||||
|
return FlairLabels(rawValue: name.lowercased())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}.sorted { $0.sortOrder < $1.sortOrder }
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonFlairLabels: [LinkedItemLabel] {
|
||||||
|
item.sortedLabels.filter { label in
|
||||||
|
if let name = label.name, FlairLabels(rawValue: name.lowercased()) != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var readInfo: some View {
|
var readInfo: some View {
|
||||||
AnyView(HStack {
|
HStack(alignment: .center, spacing: 5.0) {
|
||||||
|
ForEach(flairLabels, id: \.self) {
|
||||||
|
$0.icon
|
||||||
|
}
|
||||||
|
|
||||||
let fgcolor = Color.isDarkMode ? Color.themeDarkWhiteGray : Color.themeMiddleGray
|
let fgcolor = Color.isDarkMode ? Color.themeDarkWhiteGray : Color.themeMiddleGray
|
||||||
Text("\(estimatedReadingTime)")
|
Text("\(estimatedReadingTime)")
|
||||||
.font(.caption2).fontWeight(.medium)
|
.font(.caption2).fontWeight(.medium)
|
||||||
@ -146,7 +197,7 @@ public struct LibraryItemCard: View {
|
|||||||
.font(.caption2).fontWeight(.medium)
|
.font(.caption2).fontWeight(.medium)
|
||||||
.foregroundColor(fgcolor)
|
.foregroundColor(fgcolor)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading))
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageBox: some View {
|
var imageBox: some View {
|
||||||
@ -227,6 +278,6 @@ public struct LibraryItemCard: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var labels: some View {
|
var labels: some View {
|
||||||
LabelsFlowLayout(labels: item.sortedLabels)
|
LabelsFlowLayout(labels: nonFlairLabels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,4 +28,11 @@ public extension Image {
|
|||||||
static var unarchive: Image { Image("unarchive", bundle: .module) }
|
static var unarchive: Image { Image("unarchive", bundle: .module) }
|
||||||
static var remove: Image { Image("remove", bundle: .module) }
|
static var remove: Image { Image("remove", bundle: .module) }
|
||||||
static var label: Image { Image("label", bundle: .module) }
|
static var label: Image { Image("label", bundle: .module) }
|
||||||
|
|
||||||
|
static var flairFeed: Image { Image("flair-feed", bundle: .module) }
|
||||||
|
static var flairFavorite: Image { Image("flair-favorite", bundle: .module) }
|
||||||
|
|
||||||
|
static var flairNewsletter: Image { Image("flair-newsletter", bundle: .module) }
|
||||||
|
static var flairPinned: Image { Image("flair-pinned", bundle: .module) }
|
||||||
|
static var flairRecommended: Image { Image("flair-recommended", bundle: .module) }
|
||||||
}
|
}
|
||||||
|
|||||||
23
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-favorite.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Frame-2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame-2 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame-2 2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-favorite.imageset/Frame-2 1.png
vendored
Normal file
|
After Width: | Height: | Size: 514 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-favorite.imageset/Frame-2 2.png
vendored
Normal file
|
After Width: | Height: | Size: 739 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-favorite.imageset/Frame-2.png
vendored
Normal file
|
After Width: | Height: | Size: 339 B |
23
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-feed.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Frame.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame 2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-feed.imageset/Frame 1.png
vendored
Normal file
|
After Width: | Height: | Size: 491 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-feed.imageset/Frame 2.png
vendored
Normal file
|
After Width: | Height: | Size: 513 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-feed.imageset/Frame.png
vendored
Normal file
|
After Width: | Height: | Size: 397 B |
23
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-newsletter.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Frame-1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame-1 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame-1 2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-newsletter.imageset/Frame-1 1.png
vendored
Normal file
|
After Width: | Height: | Size: 507 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-newsletter.imageset/Frame-1 2.png
vendored
Normal file
|
After Width: | Height: | Size: 638 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-newsletter.imageset/Frame-1.png
vendored
Normal file
|
After Width: | Height: | Size: 322 B |
23
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-pinned.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Frame-3.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame-3 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame-3 2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-pinned.imageset/Frame-3 1.png
vendored
Normal file
|
After Width: | Height: | Size: 473 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-pinned.imageset/Frame-3 2.png
vendored
Normal file
|
After Width: | Height: | Size: 654 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-pinned.imageset/Frame-3.png
vendored
Normal file
|
After Width: | Height: | Size: 351 B |
23
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-recommended.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Frame-4.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame-4 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame-4 2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-recommended.imageset/Frame-4 1.png
vendored
Normal file
|
After Width: | Height: | Size: 485 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-recommended.imageset/Frame-4 2.png
vendored
Normal file
|
After Width: | Height: | Size: 621 B |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/flair-recommended.imageset/Frame-4.png
vendored
Normal file
|
After Width: | Height: | Size: 328 B |