Merge pull request #1606 from omnivore-app/feature/android-local-db

Android Persistence
This commit is contained in:
Satindar Dhillon 2023-01-04 11:40:50 -08:00 committed by GitHub
commit e6af2f5bc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 521 additions and 257 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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?,
)

View file

@ -1,9 +0,0 @@
package app.omnivore.omnivore.models
data class Viewer(
val id: String,
val name: String,
val username: String,
val pictureUrl: String?,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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?,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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