mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1606 from omnivore-app/feature/android-local-db
Android Persistence
This commit is contained in:
commit
e6af2f5bc1
41 changed files with 521 additions and 257 deletions
|
|
@ -15,10 +15,10 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "app.omnivore.omnivore"
|
||||
minSdk 23
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 16
|
||||
versionName "0.0.16"
|
||||
versionCode 19
|
||||
versionName "0.0.19"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
|
@ -143,6 +143,11 @@ dependencies {
|
|||
|
||||
implementation 'com.segment.analytics.kotlin:android:1.10.0'
|
||||
implementation 'io.intercom.android:intercom-sdk-base:14.0.0'
|
||||
|
||||
// Room Deps
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
}
|
||||
|
||||
apollo {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package app.omnivore.omnivore
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import app.omnivore.omnivore.persistence.AppDatabase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -26,4 +28,8 @@ object AppModule {
|
|||
@Singleton
|
||||
@Provides
|
||||
fun provideAnalytics(@ApplicationContext app: Context) = EventTracker(app)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideDataService(@ApplicationContext app: Context) = DataService(app)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package app.omnivore.omnivore
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import app.omnivore.omnivore.persistence.AppDatabase
|
||||
import javax.inject.Inject
|
||||
|
||||
class DataService @Inject constructor(
|
||||
context: Context
|
||||
) {
|
||||
val db = Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java, "omnivore-database"
|
||||
).build()
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ import androidx.core.view.ViewCompat
|
|||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.ui.home.HomeViewModel
|
||||
import app.omnivore.omnivore.ui.library.LibraryViewModel
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderViewModel
|
||||
import app.omnivore.omnivore.ui.root.RootView
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
|
|
@ -31,7 +31,7 @@ class MainActivity : ComponentActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
|
||||
val loginViewModel: LoginViewModel by viewModels()
|
||||
val homeViewModel: HomeViewModel by viewModels()
|
||||
val libraryViewModel: LibraryViewModel by viewModels()
|
||||
val webReaderViewModel: WebReaderViewModel by viewModels()
|
||||
|
||||
val context = this
|
||||
|
|
@ -53,7 +53,7 @@ class MainActivity : ComponentActivity() {
|
|||
.fillMaxSize()
|
||||
.background(color = Color.Black)
|
||||
) {
|
||||
RootView(loginViewModel, homeViewModel, webReaderViewModel)
|
||||
RootView(loginViewModel, libraryViewModel, webReaderViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package app.omnivore.omnivore
|
||||
|
||||
sealed class Routes(val route: String) {
|
||||
object Home : Routes("Home")
|
||||
object Library : Routes("Library")
|
||||
object WebAppReader : Routes("WebAppReader")
|
||||
object Settings: Routes("Settings")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
package app.omnivore.omnivore.models
|
||||
|
||||
data class Highlight(
|
||||
val id: String,
|
||||
val shortId: String,
|
||||
val quote: String,
|
||||
val prefix: String?,
|
||||
val suffix: String?,
|
||||
val patch: String,
|
||||
val annotation: String?,
|
||||
val createdAt: Any?,
|
||||
val updatedAt: Any?,
|
||||
val createdByMe : Boolean,
|
||||
)
|
||||
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package app.omnivore.omnivore.models
|
||||
|
||||
import androidx.core.net.toUri
|
||||
|
||||
data class LinkedItem(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val createdAt: Any,
|
||||
val savedAt: Any,
|
||||
val readAt: Any?,
|
||||
val updatedAt: Any?,
|
||||
val readingProgress: Double,
|
||||
val readingProgressAnchor: Int,
|
||||
val imageURLString: String?,
|
||||
val pageURLString: String,
|
||||
val descriptionText: String?,
|
||||
val publisherURLString: String?,
|
||||
val siteName: String?,
|
||||
val author: String?,
|
||||
val publishDate: Any?,
|
||||
val slug: String,
|
||||
val isArchived: Boolean,
|
||||
val contentReader: String?,
|
||||
val content: String?
|
||||
) {
|
||||
fun publisherDisplayName(): String? {
|
||||
return publisherURLString?.toUri()?.host
|
||||
}
|
||||
|
||||
fun isPDF(): Boolean {
|
||||
val hasPDFSuffix = pageURLString.endsWith("pdf")
|
||||
return contentReader == "PDF" || hasPDFSuffix
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package app.omnivore.omnivore.models
|
||||
|
||||
data class LinkedItemLabel(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val color: String,
|
||||
val createdAt: Any?,
|
||||
val labelDescription: String?,
|
||||
)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package app.omnivore.omnivore.models
|
||||
|
||||
data class Viewer(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val username: String,
|
||||
val pictureUrl: String?,
|
||||
)
|
||||
|
||||
|
|
@ -8,10 +8,10 @@ import app.omnivore.omnivore.graphql.generated.UpdateHighlightMutation
|
|||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import app.omnivore.omnivore.models.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
import com.pspdfkit.annotations.HighlightAnnotation
|
||||
import java.time.LocalDate
|
||||
|
||||
data class CreateHighlightParams(
|
||||
val shortId: String?,
|
||||
|
|
@ -137,6 +137,8 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
|
|||
val createdHighlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight
|
||||
|
||||
if (createdHighlight != null) {
|
||||
// val updatedAtString = createdHighlight.highlightFields.updatedAt as? String
|
||||
|
||||
return Highlight(
|
||||
id = createdHighlight.highlightFields.id,
|
||||
shortId = createdHighlight.highlightFields.shortId,
|
||||
|
|
@ -146,8 +148,10 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
|
|||
patch = createdHighlight.highlightFields.patch,
|
||||
annotation = createdHighlight.highlightFields.annotation,
|
||||
createdAt = null, // TODO: update gql query to get this
|
||||
updatedAt = createdHighlight.highlightFields.updatedAt,
|
||||
updatedAt = null, // TODO: fix updatedAtString?.let { LocalDate.parse(it) },
|
||||
createdByMe = createdHighlight.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
serverSyncStatus = 1 // TODO: create enum for this
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import app.omnivore.omnivore.graphql.generated.SetLinkArchivedMutation
|
|||
import app.omnivore.omnivore.graphql.generated.type.ArchiveLinkInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.SetBookmarkArticleInput
|
||||
|
||||
suspend fun Networker.deleteLinkedItem(itemID: String): Boolean {
|
||||
suspend fun Networker.deleteSavedItem(itemID: String): Boolean {
|
||||
return try {
|
||||
val input = SetBookmarkArticleInput(itemID, false)
|
||||
val result = authenticatedApolloClient().mutation(SetBookmarkArticleMutation(input)).execute()
|
||||
|
|
@ -15,15 +15,15 @@ suspend fun Networker.deleteLinkedItem(itemID: String): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun Networker.archiveLinkedItem(itemID: String): Boolean {
|
||||
return updateArchiveStatusLinkedItem(itemID, true)
|
||||
suspend fun Networker.archiveSavedItem(itemID: String): Boolean {
|
||||
return updateArchiveStatusSavedItem(itemID, true)
|
||||
}
|
||||
|
||||
suspend fun Networker.unarchiveLinkedItem(itemID: String): Boolean {
|
||||
return updateArchiveStatusLinkedItem(itemID, false)
|
||||
suspend fun Networker.unarchiveSavedItem(itemID: String): Boolean {
|
||||
return updateArchiveStatusSavedItem(itemID, false)
|
||||
}
|
||||
|
||||
private suspend fun Networker.updateArchiveStatusLinkedItem(itemID: String, setAsArchived: Boolean): Boolean {
|
||||
private suspend fun Networker.updateArchiveStatusSavedItem(itemID: String, setAsArchived: Boolean): Boolean {
|
||||
return try {
|
||||
val input = ArchiveLinkInput(setAsArchived, itemID)
|
||||
val result = authenticatedApolloClient().mutation(SetLinkArchivedMutation(input)).execute()
|
||||
|
|
@ -1,44 +1,46 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.GetArticleQuery
|
||||
import app.omnivore.omnivore.models.Highlight
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.models.LinkedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
|
||||
data class LinkedItemQueryResponse(
|
||||
val item: LinkedItem?,
|
||||
data class SavedItemQueryResponse(
|
||||
val item: SavedItem?,
|
||||
val highlights: List<Highlight>,
|
||||
val labels: List<LinkedItemLabel>
|
||||
val labels: List<SavedItemLabel>
|
||||
) {
|
||||
companion object {
|
||||
fun emptyResponse(): LinkedItemQueryResponse {
|
||||
return LinkedItemQueryResponse(null, listOf(), listOf())
|
||||
fun emptyResponse(): SavedItemQueryResponse {
|
||||
return SavedItemQueryResponse(null, listOf(), listOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Networker.linkedItem(slug: String): LinkedItemQueryResponse {
|
||||
suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
|
||||
try {
|
||||
val result = authenticatedApolloClient().query(
|
||||
GetArticleQuery(slug = slug)
|
||||
).execute()
|
||||
|
||||
val article = result.data?.article?.onArticleSuccess?.article
|
||||
?: return LinkedItemQueryResponse.emptyResponse()
|
||||
?: return SavedItemQueryResponse.emptyResponse()
|
||||
|
||||
val labels = article.labels ?: listOf()
|
||||
|
||||
val linkedItemLabels = labels.map {
|
||||
LinkedItemLabel(
|
||||
val savedItemLabels = labels.map {
|
||||
SavedItemLabel(
|
||||
id = it.labelFields.id,
|
||||
name = it.labelFields.name,
|
||||
color = it.labelFields.color,
|
||||
createdAt = it.labelFields.createdAt,
|
||||
createdAt = it.labelFields.createdAt as String?,
|
||||
labelDescription = it.labelFields.description
|
||||
)
|
||||
}
|
||||
|
||||
val highlights = article.highlights.map {
|
||||
// val updatedAtString = it.highlightFields.updatedAt as? String
|
||||
|
||||
Highlight(
|
||||
id = it.highlightFields.id,
|
||||
shortId = it.highlightFields.shortId,
|
||||
|
|
@ -48,20 +50,22 @@ suspend fun Networker.linkedItem(slug: String): LinkedItemQueryResponse {
|
|||
patch = it.highlightFields.patch,
|
||||
annotation = it.highlightFields.annotation,
|
||||
createdAt = null, // TODO: update gql query to get this
|
||||
updatedAt = it.highlightFields.updatedAt,
|
||||
updatedAt = null, //updatedAtString?.let { str -> LocalDate.parse(str) }, TODO: fix date parsing
|
||||
createdByMe = it.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
serverSyncStatus = 1 // TODO: create enum for this
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: handle errors
|
||||
|
||||
val linkedItem = LinkedItem(
|
||||
val savedItem = SavedItem(
|
||||
id = article.articleFields.id,
|
||||
title = article.articleFields.title,
|
||||
createdAt = article.articleFields.createdAt,
|
||||
savedAt = article.articleFields.savedAt,
|
||||
readAt = article.articleFields.readAt,
|
||||
updatedAt = article.articleFields.updatedAt,
|
||||
createdAt = article.articleFields.createdAt as String,
|
||||
savedAt = article.articleFields.savedAt as String,
|
||||
readAt = article.articleFields.readAt as String?,
|
||||
updatedAt = article.articleFields.updatedAt as String?,
|
||||
readingProgress = article.articleFields.readingProgressPercent,
|
||||
readingProgressAnchor = article.articleFields.readingProgressAnchorIndex,
|
||||
imageURLString = article.articleFields.image,
|
||||
|
|
@ -70,15 +74,15 @@ suspend fun Networker.linkedItem(slug: String): LinkedItemQueryResponse {
|
|||
publisherURLString = article.articleFields.originalArticleUrl,
|
||||
siteName = article.articleFields.siteName,
|
||||
author = article.articleFields.author,
|
||||
publishDate = article.articleFields.publishedAt,
|
||||
publishDate = article.articleFields.publishedAt as String?,
|
||||
slug = article.articleFields.slug,
|
||||
isArchived = article.articleFields.isArchived,
|
||||
contentReader = article.articleFields.contentReader.rawValue,
|
||||
content = article.articleFields.content
|
||||
)
|
||||
|
||||
return LinkedItemQueryResponse(item = linkedItem, highlights, labels = linkedItemLabels)
|
||||
return SavedItemQueryResponse(item = savedItem, highlights, labels = savedItemLabels)
|
||||
} catch (e: java.lang.Exception) {
|
||||
return LinkedItemQueryResponse(item = null, listOf(), labels = listOf())
|
||||
return SavedItemQueryResponse(item = null, listOf(), labels = listOf())
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@ package app.omnivore.omnivore.networking
|
|||
|
||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||
import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
data class SearchQueryResponse(
|
||||
val cursor: String?,
|
||||
val items: List<LinkedItem>
|
||||
val cardsData: List<SavedItemCardData>
|
||||
)
|
||||
|
||||
suspend fun Networker.typeaheadSearch(
|
||||
|
|
@ -20,31 +21,21 @@ suspend fun Networker.typeaheadSearch(
|
|||
|
||||
val itemList = result.data?.typeaheadSearch?.onTypeaheadSearchSuccess?.items ?: listOf()
|
||||
|
||||
val items = itemList.map {
|
||||
LinkedItem(
|
||||
val cardsData = itemList.map {
|
||||
SavedItemCardData(
|
||||
id = it.id,
|
||||
title = it.title,
|
||||
createdAt = "",
|
||||
savedAt = "",
|
||||
readAt = "",
|
||||
updatedAt = "",
|
||||
readingProgress = 0.0,
|
||||
readingProgressAnchor = 0,
|
||||
imageURLString = null,
|
||||
pageURLString = "",
|
||||
descriptionText = "",
|
||||
publisherURLString = "",
|
||||
siteName = it.siteName,
|
||||
author = "",
|
||||
publishDate = null,
|
||||
slug = it.slug,
|
||||
publisherURLString = "",
|
||||
title = it.title,
|
||||
author = "",
|
||||
imageURLString = null,
|
||||
isArchived = false,
|
||||
pageURLString = "",
|
||||
contentReader = null,
|
||||
content = null
|
||||
)
|
||||
}
|
||||
|
||||
return SearchQueryResponse(null, items)
|
||||
return SearchQueryResponse(null, cardsData)
|
||||
} catch (e: java.lang.Exception) {
|
||||
return SearchQueryResponse(null, listOf())
|
||||
}
|
||||
|
|
@ -68,31 +59,21 @@ suspend fun Networker.search(
|
|||
val newCursor = result.data?.search?.onSearchSuccess?.pageInfo?.endCursor
|
||||
val itemList = result.data?.search?.onSearchSuccess?.edges ?: listOf()
|
||||
|
||||
val items = itemList.map {
|
||||
LinkedItem(
|
||||
val cardsData = itemList.map {
|
||||
SavedItemCardData(
|
||||
id = it.node.id,
|
||||
title = it.node.title,
|
||||
createdAt = it.node.createdAt,
|
||||
savedAt = it.node.savedAt,
|
||||
readAt = it.node.readAt,
|
||||
updatedAt = it.node.updatedAt,
|
||||
readingProgress = it.node.readingProgressPercent,
|
||||
readingProgressAnchor = it.node.readingProgressAnchorIndex,
|
||||
imageURLString = it.node.image,
|
||||
pageURLString = it.node.url,
|
||||
descriptionText = it.node.description,
|
||||
publisherURLString = it.node.originalArticleUrl,
|
||||
siteName = it.node.siteName,
|
||||
author = it.node.author,
|
||||
publishDate = it.node.publishedAt,
|
||||
slug = it.node.slug,
|
||||
publisherURLString = it.node.originalArticleUrl,
|
||||
title = it.node.title,
|
||||
author = it.node.author,
|
||||
imageURLString = it.node.image,
|
||||
isArchived = it.node.isArchived,
|
||||
pageURLString = it.node.url,
|
||||
contentReader = it.node.contentReader.rawValue,
|
||||
content = null
|
||||
)
|
||||
}
|
||||
|
||||
return SearchQueryResponse(newCursor, items)
|
||||
return SearchQueryResponse(newCursor, cardsData)
|
||||
} catch (e: java.lang.Exception) {
|
||||
return SearchQueryResponse(null, listOf())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.ViewerQuery
|
||||
import app.omnivore.omnivore.models.Viewer
|
||||
import app.omnivore.omnivore.persistence.entities.Viewer
|
||||
|
||||
suspend fun Networker.viewer(): Viewer? {
|
||||
try {
|
||||
|
|
@ -10,10 +10,10 @@ suspend fun Networker.viewer(): Viewer? {
|
|||
|
||||
return if (me != null) {
|
||||
Viewer(
|
||||
id = me.id,
|
||||
userID = me.id,
|
||||
name = me.name,
|
||||
username = me.profile.username,
|
||||
pictureUrl = me.profile.pictureUrl
|
||||
profileImageURL = me.profile.pictureUrl
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package app.omnivore.omnivore.persistence
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemDao
|
||||
import app.omnivore.omnivore.persistence.entities.Viewer
|
||||
import app.omnivore.omnivore.persistence.entities.ViewerDao
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
Viewer::class,
|
||||
SavedItem::class
|
||||
],
|
||||
version = 1
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun viewerDao(): ViewerDao
|
||||
abstract fun savedItemDao(): SavedItemDao
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package app.omnivore.omnivore.persistence
|
||||
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Update
|
||||
|
||||
|
||||
interface BaseDao<T> {
|
||||
|
||||
/**
|
||||
* Insert an object in the database.
|
||||
*
|
||||
* @param obj the object to be inserted.
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(obj: T)
|
||||
|
||||
/**
|
||||
* Insert an array of objects in the database.
|
||||
*
|
||||
* @param obj the objects to be inserted.
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg obj: T)
|
||||
|
||||
/**
|
||||
* Update an object from the database.
|
||||
*
|
||||
* @param obj the object to be updated
|
||||
*/
|
||||
@Update
|
||||
fun update(obj: T)
|
||||
|
||||
/**
|
||||
* Delete an object from the database
|
||||
*
|
||||
* @param obj the object to be deleted
|
||||
*/
|
||||
@Delete
|
||||
fun delete(obj: T)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.time.LocalDate
|
||||
import java.util.Date
|
||||
|
||||
@Entity
|
||||
data class Highlight(
|
||||
@PrimaryKey val id: String,
|
||||
val annotation: String?,
|
||||
val createdAt: Date?,
|
||||
val createdByMe: Boolean,
|
||||
val markedForDeletion: Boolean, // default false
|
||||
val patch: String,
|
||||
val prefix: String?,
|
||||
val quote: String,
|
||||
val serverSyncStatus: Int, // default 0
|
||||
val shortId: String,
|
||||
val suffix: String?,
|
||||
val updatedAt: LocalDate?
|
||||
|
||||
// has many SavedItemLabels (inverse: labels have many highlights)
|
||||
// has one savedItem (inverse: savedItem has many highlights
|
||||
// has a UserProfile (no inverse)
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class NewsletterEmail(
|
||||
@PrimaryKey val userID: String,
|
||||
val confirmationCode: String?,
|
||||
val email: String?,
|
||||
val emailID: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class RecentSearchItem(
|
||||
@PrimaryKey val id: String,
|
||||
val savedAt: String?,
|
||||
val term: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Recommendation(
|
||||
@PrimaryKey val groupID: String,
|
||||
val name: String?,
|
||||
val note: String?,
|
||||
val recommendedAt: String?
|
||||
)
|
||||
|
||||
// hasOne SavedItem
|
||||
// hasOne UserProfile
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class RecommendationGroup(
|
||||
@PrimaryKey val id: String,
|
||||
val name: String?,
|
||||
val canPost: Boolean,
|
||||
val canSeeMembers: Boolean,
|
||||
val inviteURL: String?,
|
||||
val createdAt: String?,
|
||||
val updatedAt: String?
|
||||
)
|
||||
|
||||
// hasMany admins (Viewer)
|
||||
// hasMany members (Viewer)
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import androidx.room.*
|
||||
import app.omnivore.omnivore.persistence.BaseDao
|
||||
|
||||
@Entity
|
||||
data class SavedItem(
|
||||
@PrimaryKey val id: String,
|
||||
val title: String,
|
||||
val createdAt: String,
|
||||
val savedAt: String,
|
||||
val readAt: String?,
|
||||
val updatedAt: String?,
|
||||
val readingProgress: Double,
|
||||
val readingProgressAnchor: Int,
|
||||
val imageURLString: String?,
|
||||
val pageURLString: String,
|
||||
val descriptionText: String?,
|
||||
val publisherURLString: String?,
|
||||
val siteName: String?,
|
||||
val author: String?,
|
||||
val publishDate: String?,
|
||||
val slug: String,
|
||||
val isArchived: Boolean,
|
||||
val contentReader: String? = null,
|
||||
val content: String? = null,
|
||||
val createdId: String? = null,
|
||||
val htmlContent: String? = null,
|
||||
val language: String? = null,
|
||||
val listenPositionIndex: Int? = null,
|
||||
val listenPositionOffset: Double? = null,
|
||||
val listenPositionTime: Double? = null,
|
||||
val localPDF: String? = null,
|
||||
val onDeviceImageURLString: String? = null,
|
||||
val originalHtml: String? = null,
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) val pdfData: ByteArray? = null,
|
||||
val serverSyncStatus: Int = 0, // TODO: implement,
|
||||
val tempPDFURL: String? = null
|
||||
|
||||
// hasMany highlights
|
||||
// hasMany labels
|
||||
// has Many recommendations (rec has one savedItem)
|
||||
) {
|
||||
fun publisherDisplayName(): String? {
|
||||
return publisherURLString?.toUri()?.host
|
||||
}
|
||||
|
||||
fun isPDF(): Boolean {
|
||||
val hasPDFSuffix = pageURLString.endsWith("pdf")
|
||||
return contentReader == "PDF" || hasPDFSuffix
|
||||
}
|
||||
|
||||
fun asSavedItemCardData(): SavedItemCardData {
|
||||
return SavedItemCardData(
|
||||
id = id,
|
||||
slug = slug,
|
||||
publisherURLString = publisherURLString,
|
||||
title = title,
|
||||
author = author,
|
||||
imageURLString = imageURLString,
|
||||
isArchived = isArchived,
|
||||
pageURLString = pageURLString,
|
||||
contentReader = contentReader,
|
||||
)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SavedItem
|
||||
|
||||
if (id != other.id) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
data class SavedItemCardData(
|
||||
val id: String,
|
||||
val slug: String,
|
||||
val publisherURLString: String?,
|
||||
val title: String,
|
||||
val author: String?,
|
||||
val imageURLString: String?,
|
||||
val isArchived: Boolean,
|
||||
val pageURLString: String,
|
||||
val contentReader: String?
|
||||
) {
|
||||
fun publisherDisplayName(): String? {
|
||||
return publisherURLString?.toUri()?.host
|
||||
}
|
||||
|
||||
fun isPDF(): Boolean {
|
||||
val hasPDFSuffix = pageURLString.endsWith("pdf")
|
||||
return contentReader == "PDF" || hasPDFSuffix
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface SavedItemDao {
|
||||
@Query("SELECT id, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem")
|
||||
fun getLibraryData(): List<SavedItemCardData>
|
||||
|
||||
@Query("SELECT * FROM savedItem")
|
||||
fun getAll(): List<SavedItem>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItem>)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class SavedItemLabel(
|
||||
@PrimaryKey val id: String,
|
||||
val name: String,
|
||||
val color: String,
|
||||
val createdAt: String?,
|
||||
val labelDescription: String?,
|
||||
val serverSyncStatus: Int = 0
|
||||
)
|
||||
|
||||
// has many highlights
|
||||
// has many savedItems
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class UserProfile(
|
||||
@PrimaryKey val userID: String,
|
||||
val name: String?,
|
||||
val username: String?,
|
||||
val profileImageURL: String?,
|
||||
)
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.*
|
||||
import app.omnivore.omnivore.persistence.BaseDao
|
||||
|
||||
@Entity
|
||||
data class Viewer(
|
||||
@PrimaryKey val userID: String,
|
||||
val name: String,
|
||||
val username: String,
|
||||
val profileImageURL: String?,
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface ViewerDao {
|
||||
@Query("SELECT * FROM viewer")
|
||||
fun getAll(): List<Viewer>
|
||||
|
||||
@Query("SELECT * FROM viewer WHERE userID IN (:viewerIds)")
|
||||
fun loadAllByIds(viewerIds: IntArray): List<Viewer>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(vararg viewers: Viewer)
|
||||
|
||||
@Delete
|
||||
fun delete(viewer: Viewer)
|
||||
}
|
||||
|
|
@ -4,15 +4,11 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import app.omnivore.omnivore.*
|
||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||
import app.omnivore.omnivore.graphql.generated.ValidateUsernameQuery
|
||||
import app.omnivore.omnivore.networking.LinkedItemQueryResponse
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import app.omnivore.omnivore.networking.viewer
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
import com.google.android.gms.tasks.Task
|
||||
|
|
@ -100,7 +96,7 @@ class LoginViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
val viewer = networker.viewer()
|
||||
viewer?.let {
|
||||
eventTracker.registerUser(viewer.id)
|
||||
eventTracker.registerUser(viewer.userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.home
|
||||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -20,31 +20,32 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.ui.linkedItemViews.LinkedItemCard
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard
|
||||
import app.omnivore.omnivore.ui.reader.PDFReaderActivity
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeView(
|
||||
homeViewModel: HomeViewModel,
|
||||
fun LibraryView(
|
||||
libraryViewModel: LibraryViewModel,
|
||||
navController: NavHostController
|
||||
) {
|
||||
val searchText: String by homeViewModel.searchTextLiveData.observeAsState("")
|
||||
val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("")
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchBar(
|
||||
searchText = searchText,
|
||||
onSearchTextChanged = { homeViewModel.updateSearchText(it) },
|
||||
onSearchTextChanged = { libraryViewModel.updateSearchText(it) },
|
||||
onSettingsIconClick = { navController.navigate(Routes.Settings.route) }
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
HomeViewContent(
|
||||
homeViewModel,
|
||||
LibraryViewContent(
|
||||
libraryViewModel,
|
||||
navController,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
|
|
@ -57,8 +58,8 @@ fun HomeView(
|
|||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun HomeViewContent(
|
||||
homeViewModel: HomeViewModel,
|
||||
fun LibraryViewContent(
|
||||
libraryViewModel: LibraryViewModel,
|
||||
navController: NavHostController,
|
||||
modifier: Modifier
|
||||
) {
|
||||
|
|
@ -66,11 +67,11 @@ fun HomeViewContent(
|
|||
val listState = rememberLazyListState()
|
||||
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = homeViewModel.isRefreshing,
|
||||
onRefresh = { homeViewModel.refresh() }
|
||||
refreshing = libraryViewModel.isRefreshing,
|
||||
onRefresh = { libraryViewModel.refresh() }
|
||||
)
|
||||
|
||||
val linkedItems: List<LinkedItem> by homeViewModel.itemsLiveData.observeAsState(listOf())
|
||||
val cardsData: List<SavedItemCardData> by libraryViewModel.itemsLiveData.observeAsState(listOf())
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -86,29 +87,29 @@ fun HomeViewContent(
|
|||
.fillMaxSize()
|
||||
.padding(horizontal = 6.dp)
|
||||
) {
|
||||
items(linkedItems) { item ->
|
||||
LinkedItemCard(
|
||||
item = item,
|
||||
items(cardsData) { cardData ->
|
||||
SavedItemCard(
|
||||
cardData = cardData,
|
||||
onClickHandler = {
|
||||
if (item.isPDF()) {
|
||||
if (cardData.isPDF()) {
|
||||
val intent = Intent(context, PDFReaderActivity::class.java)
|
||||
intent.putExtra("LINKED_ITEM_SLUG", item.slug)
|
||||
intent.putExtra("SAVED_ITEM_SLUG", cardData.slug)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
navController.navigate("WebReader/${item.slug}")
|
||||
navController.navigate("WebReader/${cardData.slug}")
|
||||
}
|
||||
},
|
||||
actionHandler = { homeViewModel.handleLinkedItemAction(item.id, it) }
|
||||
actionHandler = { libraryViewModel.handleSavedItemAction(cardData.id, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
InfiniteListHandler(listState = listState) {
|
||||
homeViewModel.load()
|
||||
libraryViewModel.load()
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = homeViewModel.isRefreshing,
|
||||
refreshing = libraryViewModel.isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.home
|
||||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -7,22 +7,24 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.DataService
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import com.pspdfkit.analytics.Analytics
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
class LibraryViewModel @Inject constructor(
|
||||
private val networker: Networker,
|
||||
private val dataService: DataService
|
||||
): ViewModel() {
|
||||
private var cursor: String? = null
|
||||
private var items: List<LinkedItem> = listOf()
|
||||
private var searchedItems: List<LinkedItem> = listOf()
|
||||
private var items: List<SavedItemCardData> = listOf()
|
||||
private var searchedItems: List<SavedItemCardData> = listOf()
|
||||
|
||||
// These are used to make sure we handle search result
|
||||
// responses in the right order
|
||||
|
|
@ -31,7 +33,7 @@ class HomeViewModel @Inject constructor(
|
|||
|
||||
// Live Data
|
||||
val searchTextLiveData = MutableLiveData("")
|
||||
val itemsLiveData = MutableLiveData<List<LinkedItem>>(listOf())
|
||||
val itemsLiveData = MutableLiveData<List<SavedItemCardData>>(listOf())
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
|
||||
fun updateSearchText(text: String) {
|
||||
|
|
@ -80,38 +82,44 @@ class HomeViewModel @Inject constructor(
|
|||
|
||||
if (searchTextLiveData.value != "" || clearPreviousSearch) {
|
||||
val previousItems = if (clearPreviousSearch) listOf() else searchedItems
|
||||
searchedItems = previousItems.plus(searchResult.items)
|
||||
searchedItems = previousItems.plus(searchResult.cardsData)
|
||||
itemsLiveData.postValue(searchedItems)
|
||||
} else {
|
||||
items = items.plus(searchResult.items)
|
||||
items = items.plus(searchResult.cardsData)
|
||||
itemsLiveData.postValue(items)
|
||||
}
|
||||
|
||||
// withContext(Dispatchers.IO) {
|
||||
// dataService.db.savedItemDao().insertAll(items)
|
||||
// val items = dataService.db.savedItemDao().getLibraryData()
|
||||
// Log.d("appDatabase", "libraryData: $items")
|
||||
// }
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleLinkedItemAction(itemID: String, action: LinkedItemAction) {
|
||||
fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
|
||||
when (action) {
|
||||
LinkedItemAction.Delete -> {
|
||||
SavedItemAction.Delete -> {
|
||||
removeItemFromList(itemID)
|
||||
|
||||
viewModelScope.launch {
|
||||
networker.deleteLinkedItem(itemID)
|
||||
networker.deleteSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
LinkedItemAction.Archive -> {
|
||||
SavedItemAction.Archive -> {
|
||||
removeItemFromList(itemID)
|
||||
viewModelScope.launch {
|
||||
networker.archiveLinkedItem(itemID)
|
||||
networker.archiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
LinkedItemAction.Unarchive -> {
|
||||
SavedItemAction.Unarchive -> {
|
||||
removeItemFromList(itemID)
|
||||
viewModelScope.launch {
|
||||
networker.unarchiveLinkedItem(itemID)
|
||||
networker.unarchiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -135,7 +143,7 @@ class HomeViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
enum class LinkedItemAction {
|
||||
enum class SavedItemAction {
|
||||
Delete,
|
||||
Archive,
|
||||
Unarchive
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.home
|
||||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
|
|
@ -38,7 +38,7 @@ fun SearchBar(
|
|||
if (showSearchField) {
|
||||
SearchField(searchText, onSearchTextChanged)
|
||||
} else {
|
||||
Text("Home")
|
||||
Text("Library")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(
|
||||
|
|
@ -19,7 +19,7 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.models.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import com.pspdfkit.annotations.Annotation
|
||||
import com.pspdfkit.annotations.HighlightAnnotation
|
||||
import com.pspdfkit.configuration.PdfConfiguration
|
||||
|
|
@ -78,7 +78,7 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan
|
|||
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
|
||||
viewModel.pdfReaderParamsLiveData.observe(this, pdfParamsObserver)
|
||||
|
||||
val slug = intent.getStringExtra("LINKED_ITEM_SLUG") ?: ""
|
||||
val slug = intent.getStringExtra("SAVED_ITEM_SLUG") ?: ""
|
||||
viewModel.loadItem(slug, this)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import app.omnivore.omnivore.DatastoreRepository
|
|||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
|
|
@ -29,7 +29,7 @@ import java.util.*
|
|||
import javax.inject.Inject
|
||||
|
||||
data class PDFReaderParams(
|
||||
val item: LinkedItem,
|
||||
val item: SavedItem,
|
||||
val articleContent: ArticleContent,
|
||||
val localFileUri: Uri
|
||||
)
|
||||
|
|
@ -45,7 +45,7 @@ class PDFReaderViewModel @Inject constructor(
|
|||
|
||||
fun loadItem(slug: String, context: Context) {
|
||||
viewModelScope.launch {
|
||||
val articleQueryResult = networker.linkedItem(slug)
|
||||
val articleQueryResult = networker.savedItem(slug)
|
||||
|
||||
val article = articleQueryResult.item ?: return@launch
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
|
|
@ -29,14 +28,12 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.linkedItemViews.LinkedItemContextMenu
|
||||
import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
|
@ -112,11 +109,11 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod
|
|||
contentDescription = null
|
||||
)
|
||||
}
|
||||
LinkedItemContextMenu(
|
||||
SavedItemContextMenu(
|
||||
isExpanded = isMenuExpanded,
|
||||
isArchived = webReaderParams!!.item.isArchived,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
actionHandler = { webReaderViewModel.handleLinkedItemAction(webReaderParams!!.item.id, it) }
|
||||
actionHandler = { webReaderViewModel.handleSavedItemAction(webReaderParams!!.item.id, it) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.models.Highlight
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import com.google.gson.Gson
|
||||
|
||||
enum class WebFont(val displayText: String, val rawValue: String) {
|
||||
|
|
@ -39,7 +39,7 @@ data class ArticleContent(
|
|||
|
||||
data class WebReaderContent(
|
||||
val preferences: WebPreferences,
|
||||
val item: LinkedItem,
|
||||
val item: SavedItem,
|
||||
val articleContent: ArticleContent,
|
||||
) {
|
||||
fun styledContent(): String {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@ package app.omnivore.omnivore.ui.reader
|
|||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.ui.home.LinkedItemAction
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -21,7 +20,7 @@ import java.util.*
|
|||
import javax.inject.Inject
|
||||
|
||||
data class WebReaderParams(
|
||||
val item: LinkedItem,
|
||||
val item: SavedItem,
|
||||
val articleContent: ArticleContent
|
||||
)
|
||||
|
||||
|
|
@ -48,7 +47,7 @@ class WebReaderViewModel @Inject constructor(
|
|||
|
||||
fun loadItem(slug: String) {
|
||||
viewModelScope.launch {
|
||||
val articleQueryResult = networker.linkedItem(slug)
|
||||
val articleQueryResult = networker.savedItem(slug)
|
||||
|
||||
val article = articleQueryResult.item ?: return@launch
|
||||
|
||||
|
|
@ -65,33 +64,33 @@ class WebReaderViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleLinkedItemAction(itemID: String, action: LinkedItemAction) {
|
||||
fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
|
||||
when (action) {
|
||||
LinkedItemAction.Delete -> {
|
||||
SavedItemAction.Delete -> {
|
||||
viewModelScope.launch {
|
||||
networker.deleteLinkedItem(itemID)
|
||||
popToHomeView(itemID)
|
||||
networker.deleteSavedItem(itemID)
|
||||
popToLibraryView(itemID)
|
||||
}
|
||||
}
|
||||
LinkedItemAction.Archive -> {
|
||||
SavedItemAction.Archive -> {
|
||||
viewModelScope.launch {
|
||||
networker.archiveLinkedItem(itemID)
|
||||
popToHomeView(itemID)
|
||||
networker.archiveSavedItem(itemID)
|
||||
popToLibraryView(itemID)
|
||||
}
|
||||
}
|
||||
LinkedItemAction.Unarchive -> {
|
||||
SavedItemAction.Unarchive -> {
|
||||
viewModelScope.launch {
|
||||
networker.unarchiveLinkedItem(itemID)
|
||||
popToHomeView(itemID)
|
||||
networker.unarchiveSavedItem(itemID)
|
||||
popToLibraryView(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun popToHomeView(itemID: String) {
|
||||
private fun popToLibraryView(itemID: String) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
// TODO: pop to home
|
||||
Log.d("maxx", "should pop to home and remove item with ID: $itemID")
|
||||
// TODO: pop to library
|
||||
Log.d("maxx", "should pop to library and remove item with ID: $itemID")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,15 +16,15 @@ import androidx.navigation.compose.rememberNavController
|
|||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.ui.auth.WelcomeScreen
|
||||
import app.omnivore.omnivore.ui.home.HomeView
|
||||
import app.omnivore.omnivore.ui.home.HomeViewModel
|
||||
import app.omnivore.omnivore.ui.library.LibraryView
|
||||
import app.omnivore.omnivore.ui.library.LibraryViewModel
|
||||
import app.omnivore.omnivore.ui.reader.*
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
||||
@Composable
|
||||
fun RootView(
|
||||
loginViewModel: LoginViewModel,
|
||||
homeViewModel: HomeViewModel,
|
||||
libraryViewModel: LibraryViewModel,
|
||||
webReaderViewModel: WebReaderViewModel
|
||||
) {
|
||||
val hasAuthToken: Boolean by loginViewModel.hasAuthTokenLiveData.observeAsState(false)
|
||||
|
|
@ -47,7 +47,7 @@ fun RootView(
|
|||
if (hasAuthToken) {
|
||||
PrimaryNavigator(
|
||||
loginViewModel = loginViewModel,
|
||||
homeViewModel = homeViewModel,
|
||||
libraryViewModel = libraryViewModel,
|
||||
webReaderViewModel = webReaderViewModel
|
||||
)
|
||||
} else {
|
||||
|
|
@ -66,15 +66,15 @@ fun RootView(
|
|||
@Composable
|
||||
fun PrimaryNavigator(
|
||||
loginViewModel: LoginViewModel,
|
||||
homeViewModel: HomeViewModel,
|
||||
libraryViewModel: LibraryViewModel,
|
||||
webReaderViewModel: WebReaderViewModel
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = Routes.Home.route) {
|
||||
composable(Routes.Home.route) {
|
||||
HomeView(
|
||||
homeViewModel = homeViewModel,
|
||||
NavHost(navController = navController, startDestination = Routes.Library.route) {
|
||||
composable(Routes.Library.route) {
|
||||
LibraryView(
|
||||
libraryViewModel = libraryViewModel,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.linkedItemViews
|
||||
package app.omnivore.omnivore.ui.savedItemViews
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -12,15 +12,16 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.ui.home.LinkedItemAction
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LinkedItemCard(item: LinkedItem, onClickHandler: () -> Unit, actionHandler: (LinkedItemAction) -> Unit) {
|
||||
fun SavedItemCard(cardData: SavedItemCardData, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
val publisherDisplayName = item.publisherDisplayName()
|
||||
val publisherDisplayName = cardData.publisherDisplayName()
|
||||
|
||||
Column {
|
||||
Row(
|
||||
|
|
@ -42,14 +43,14 @@ fun LinkedItemCard(item: LinkedItem, onClickHandler: () -> Unit, actionHandler:
|
|||
.padding(end = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item.title,
|
||||
text = cardData.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
if (item.author != null && item.author != "") {
|
||||
if (cardData.author != null && cardData.author != "") {
|
||||
Text(
|
||||
text = "By ${item.author}",
|
||||
text = "By ${cardData.author}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
|
@ -66,10 +67,10 @@ fun LinkedItemCard(item: LinkedItem, onClickHandler: () -> Unit, actionHandler:
|
|||
}
|
||||
}
|
||||
|
||||
if (item.imageURLString != null) {
|
||||
if (cardData.imageURLString != null) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(item.imageURLString),
|
||||
contentDescription = "Image associated with linked item",
|
||||
painter = rememberAsyncImagePainter(cardData.imageURLString),
|
||||
contentDescription = "Image associated with saved item",
|
||||
modifier = Modifier
|
||||
.padding(top = 6.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
|
|
@ -80,9 +81,9 @@ fun LinkedItemCard(item: LinkedItem, onClickHandler: () -> Unit, actionHandler:
|
|||
|
||||
Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp)
|
||||
|
||||
LinkedItemContextMenu(
|
||||
SavedItemContextMenu(
|
||||
isExpanded = isMenuExpanded,
|
||||
isArchived = item.isArchived,
|
||||
isArchived = cardData.isArchived,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
actionHandler = actionHandler
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.linkedItemViews
|
||||
package app.omnivore.omnivore.ui.savedItemViews
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
|
|
@ -8,14 +8,14 @@ import androidx.compose.material3.DropdownMenuItem
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import app.omnivore.omnivore.ui.home.LinkedItemAction
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
|
||||
@Composable
|
||||
fun LinkedItemContextMenu(
|
||||
fun SavedItemContextMenu(
|
||||
isExpanded: Boolean,
|
||||
isArchived: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
actionHandler: (LinkedItemAction) -> Unit
|
||||
actionHandler: (SavedItemAction) -> Unit
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
|
|
@ -24,7 +24,7 @@ fun LinkedItemContextMenu(
|
|||
DropdownMenuItem(
|
||||
text = { Text(if (isArchived) "Unarchive" else "Archive") },
|
||||
onClick = {
|
||||
val action = if (isArchived) LinkedItemAction.Unarchive else LinkedItemAction.Archive
|
||||
val action = if (isArchived) SavedItemAction.Unarchive else SavedItemAction.Archive
|
||||
actionHandler(action)
|
||||
onDismiss()
|
||||
},
|
||||
|
|
@ -38,7 +38,7 @@ fun LinkedItemContextMenu(
|
|||
DropdownMenuItem(
|
||||
text = { Text("Remove Item") },
|
||||
onClick = {
|
||||
actionHandler(LinkedItemAction.Delete)
|
||||
actionHandler(SavedItemAction.Delete)
|
||||
onDismiss()
|
||||
},
|
||||
leadingIcon = {
|
||||
|
|
@ -1,14 +1,11 @@
|
|||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.TopAppBarDefaults.smallTopAppBarColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
|
|
@ -34,7 +31,7 @@ fun SettingsView(
|
|||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
actions = {
|
||||
IconButton(onClick = { navController.navigate(Routes.Home.route) }) {
|
||||
IconButton(onClick = { navController.navigate(Routes.Library.route) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Home,
|
||||
contentDescription = null
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ buildscript {
|
|||
lifecycle_version = '2.5.1'
|
||||
hilt_version = '2.44.2'
|
||||
gradle_plugin_version = '7.3.1'
|
||||
room_version = '2.4.3'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
Loading…
Reference in a new issue