Merge pull request #2981 from omnivore-app/feat/label-flair

First pass at label flair for iOS and Android
This commit is contained in:
Jackson Harper
2023-10-24 12:42:57 +08:00
committed by GitHub
33 changed files with 401 additions and 80 deletions

View File

@ -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 {

View File

@ -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
} }
} }

View File

@ -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
} }
} }

View File

@ -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 {

View File

@ -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(

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View 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="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>

View File

@ -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 = "";

View File

@ -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)
} }
} }

View File

@ -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) }
} }

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B