First pass at label flair for iOS and Android

This commit is contained in:
Jackson Harper
2023-10-20 17:54:20 +08:00
parent b27ff99bff
commit 6a4d2a57f0
33 changed files with 401 additions and 80 deletions

View File

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

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

View File

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

View File

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

View File

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

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",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.34.0;
MARKETING_VERSION = 1.35.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
@ -1435,7 +1435,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.34.0;
MARKETING_VERSION = 1.35.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1490,7 +1490,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.34.0;
MARKETING_VERSION = 1.35.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1831,7 +1831,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.34.0;
MARKETING_VERSION = 1.35.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -2,6 +2,35 @@ import Models
import SwiftUI
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 {
func draggableItem(item: LinkedItem) -> some View {
#if os(iOS)
@ -124,8 +153,30 @@ public struct LibraryItemCard: View {
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 {
AnyView(HStack {
HStack(alignment: .center, spacing: 5.0) {
ForEach(flairLabels, id: \.self) {
$0.icon
}
let fgcolor = Color.isDarkMode ? Color.themeDarkWhiteGray : Color.themeMiddleGray
Text("\(estimatedReadingTime)")
.font(.caption2).fontWeight(.medium)
@ -146,7 +197,7 @@ public struct LibraryItemCard: View {
.font(.caption2).fontWeight(.medium)
.foregroundColor(fgcolor)
}
.frame(maxWidth: .infinity, alignment: .leading))
.frame(maxWidth: .infinity, alignment: .leading)
}
var imageBox: some View {
@ -227,6 +278,6 @@ public struct LibraryItemCard: 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 remove: Image { Image("remove", 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