mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1530 from omnivore-app/feature/android-home-refresh
Pull to Refresh - Android
This commit is contained in:
commit
bc3e8853e8
24 changed files with 445 additions and 74 deletions
|
|
@ -17,8 +17,8 @@ android {
|
|||
applicationId "app.omnivore.omnivore"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 9
|
||||
versionName "0.0.10"
|
||||
versionCode 15
|
||||
versionName "0.0.15"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
|
@ -140,6 +140,9 @@ dependencies {
|
|||
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'com.pspdfkit:pspdfkit:8.4.1'
|
||||
|
||||
implementation 'com.segment.analytics.kotlin:android:1.10.0'
|
||||
implementation 'io.intercom.android:intercom-sdk-base:14.0.0'
|
||||
}
|
||||
|
||||
apollo {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
package="app.omnivore.omnivore">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
|
||||
|
||||
<application
|
||||
android:name=".OmnivoreApplication"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
mutation SetLinkArchived($input: ArchiveLinkInput!) {
|
||||
setLinkArchived(input: $input) {
|
||||
... on ArchiveLinkSuccess {
|
||||
linkId
|
||||
message
|
||||
}
|
||||
... on ArchiveLinkError {
|
||||
message
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
mutation SetBookmarkArticle($input: SetBookmarkArticleInput!) {
|
||||
setBookmarkArticle(input: $input) {
|
||||
... on SetBookmarkArticleSuccess {
|
||||
bookmarkedArticle {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on SetBookmarkArticleError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
13
android/Omnivore/app/src/main/graphql/Viewer.graphql
Normal file
13
android/Omnivore/app/src/main/graphql/Viewer.graphql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
query Viewer {
|
||||
me {
|
||||
id
|
||||
name
|
||||
isFullUser
|
||||
profile {
|
||||
id
|
||||
username
|
||||
pictureUrl
|
||||
bio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,4 +22,8 @@ object AppModule {
|
|||
@Singleton
|
||||
@Provides
|
||||
fun provideNetworker(datastore: DatastoreRepository) = Networker(datastore)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAnalytics(@ApplicationContext app: Context) = EventTracker(app)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package app.omnivore.omnivore
|
||||
|
||||
import android.content.Context
|
||||
import com.segment.analytics.kotlin.android.Analytics
|
||||
import com.segment.analytics.kotlin.core.*
|
||||
import io.intercom.android.sdk.Intercom
|
||||
import io.intercom.android.sdk.identity.Registration
|
||||
import org.json.JSONObject
|
||||
import javax.inject.Inject
|
||||
|
||||
class EventTracker @Inject constructor(val app: Context) {
|
||||
val segmentAnalytics: Analytics
|
||||
|
||||
init {
|
||||
val writeKey = app.getString(R.string.segment_write_key)
|
||||
|
||||
segmentAnalytics = Analytics(writeKey, app.applicationContext) {
|
||||
trackApplicationLifecycleEvents = true
|
||||
application = app.applicationContext
|
||||
useLifecycleObserver = true
|
||||
}
|
||||
}
|
||||
|
||||
fun registerUser(userID: String) {
|
||||
segmentAnalytics.identify(userID)
|
||||
Intercom.client().loginIdentifiedUser(Registration.create().withUserId(userID))
|
||||
}
|
||||
|
||||
fun debugMessage(message: String) {
|
||||
track(message)
|
||||
}
|
||||
|
||||
fun track(eventName: String, jsonObject: JSONObject = JSONObject()) {
|
||||
segmentAnalytics.track(eventName, jsonObject)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,17 @@ package app.omnivore.omnivore
|
|||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import io.intercom.android.sdk.Intercom
|
||||
|
||||
@HiltAndroidApp
|
||||
class OmnivoreApplication: Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Intercom.initialize(
|
||||
this,
|
||||
this.getString(R.string.intercom_api_key),
|
||||
this.getString(R.string.intercom_app_id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package app.omnivore.omnivore.models
|
||||
|
||||
data class Viewer(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val username: String,
|
||||
val pictureUrl: String?,
|
||||
)
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.SetBookmarkArticleMutation
|
||||
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 {
|
||||
val input = SetBookmarkArticleInput(itemID, false)
|
||||
val result = authenticatedApolloClient().mutation(SetBookmarkArticleMutation(input)).execute()
|
||||
return result.data?.setBookmarkArticle?.onSetBookmarkArticleSuccess?.bookmarkedArticle?.id != null
|
||||
}
|
||||
|
||||
suspend fun Networker.archiveLinkedItem(itemID: String): Boolean {
|
||||
return updateArchiveStatusLinkedItem(itemID, true)
|
||||
}
|
||||
|
||||
suspend fun Networker.unarchiveLinkedItem(itemID: String): Boolean {
|
||||
return updateArchiveStatusLinkedItem(itemID, false)
|
||||
}
|
||||
|
||||
private suspend fun Networker.updateArchiveStatusLinkedItem(itemID: String, setAsArchived: Boolean): Boolean {
|
||||
val input = ArchiveLinkInput(setAsArchived, itemID)
|
||||
val result = authenticatedApolloClient().mutation(SetLinkArchivedMutation(input)).execute()
|
||||
return result.data?.setLinkArchived?.onArchiveLinkSuccess?.linkId != null
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.ViewerQuery
|
||||
import app.omnivore.omnivore.models.Viewer
|
||||
|
||||
suspend fun Networker.viewer(): Viewer? {
|
||||
val result = authenticatedApolloClient().query(ViewerQuery()).execute()
|
||||
val me = result.data?.me
|
||||
|
||||
return if (me != null) {
|
||||
Viewer(
|
||||
id = me.id,
|
||||
name = me.name,
|
||||
username = me.profile.username,
|
||||
pictureUrl = me.profile.pictureUrl
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -8,12 +8,15 @@ 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.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
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.intercom.android.sdk.Intercom
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -36,7 +39,9 @@ data class PendingEmailUserCreds(
|
|||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
private val eventTracker: EventTracker,
|
||||
private val networker: Networker
|
||||
): ViewModel() {
|
||||
private var validateUsernameJob: Job? = null
|
||||
|
||||
|
|
@ -90,6 +95,15 @@ class LoginViewModel @Inject constructor(
|
|||
showSocialLogin()
|
||||
}
|
||||
|
||||
fun registerUser() {
|
||||
viewModelScope.launch {
|
||||
val viewer = networker.viewer()
|
||||
viewer?.let {
|
||||
eventTracker.registerUser(viewer.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetState() {
|
||||
validateUsernameJob = null
|
||||
isLoading = false
|
||||
|
|
@ -255,6 +269,7 @@ class LoginViewModel @Inject constructor(
|
|||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
datastoreRepo.clear()
|
||||
Intercom.client().logout()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
|
|
@ -17,6 +21,7 @@ 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.ui.reader.PDFReaderActivity
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
|
|
@ -50,6 +55,7 @@ fun HomeView(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun HomeViewContent(
|
||||
homeViewModel: HomeViewModel,
|
||||
|
|
@ -58,35 +64,54 @@ fun HomeViewContent(
|
|||
) {
|
||||
val context = LocalContext.current
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = homeViewModel.isRefreshing,
|
||||
onRefresh = { homeViewModel.refresh() }
|
||||
)
|
||||
|
||||
val linkedItems: List<LinkedItem> by homeViewModel.itemsLiveData.observeAsState(listOf())
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 6.dp)
|
||||
.pullRefresh(pullRefreshState)
|
||||
) {
|
||||
items(linkedItems) { item ->
|
||||
LinkedItemCard(
|
||||
item = item,
|
||||
onClickHandler = {
|
||||
if (item.isPDF()) {
|
||||
val intent = Intent(context, PDFReaderActivity::class.java)
|
||||
intent.putExtra("LINKED_ITEM_SLUG", item.slug)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
navController.navigate("WebReader/${item.slug}")
|
||||
}
|
||||
}
|
||||
)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 6.dp)
|
||||
) {
|
||||
items(linkedItems) { item ->
|
||||
LinkedItemCard(
|
||||
item = item,
|
||||
onClickHandler = {
|
||||
if (item.isPDF()) {
|
||||
val intent = Intent(context, PDFReaderActivity::class.java)
|
||||
intent.putExtra("LINKED_ITEM_SLUG", item.slug)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
navController.navigate("WebReader/${item.slug}")
|
||||
}
|
||||
},
|
||||
actionHandler = { homeViewModel.handleLinkedItemAction(item.id, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InfiniteListHandler(listState = listState) {
|
||||
homeViewModel.load()
|
||||
InfiniteListHandler(listState = listState) {
|
||||
homeViewModel.load()
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = homeViewModel.isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
package app.omnivore.omnivore.ui.home
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.networking.Networker
|
||||
import app.omnivore.omnivore.networking.search
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import com.pspdfkit.analytics.Analytics
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val networker: Networker
|
||||
private val networker: Networker,
|
||||
): ViewModel() {
|
||||
private var cursor: String? = null
|
||||
private var items: List<LinkedItem> = listOf()
|
||||
|
|
@ -26,6 +32,7 @@ class HomeViewModel @Inject constructor(
|
|||
// Live Data
|
||||
val searchTextLiveData = MutableLiveData("")
|
||||
val itemsLiveData = MutableLiveData<List<LinkedItem>>(listOf())
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
|
||||
fun updateSearchText(text: String) {
|
||||
searchTextLiveData.value = text
|
||||
|
|
@ -37,6 +44,11 @@ class HomeViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
isRefreshing = true
|
||||
load(true)
|
||||
}
|
||||
|
||||
fun load(clearPreviousSearch: Boolean = false) {
|
||||
if (clearPreviousSearch) {
|
||||
cursor = null
|
||||
|
|
@ -61,14 +73,49 @@ class HomeViewModel @Inject constructor(
|
|||
receivedIdx = thisSearchIdx
|
||||
cursor = searchResult.cursor
|
||||
|
||||
if (searchTextLiveData.value != "") {
|
||||
if (searchTextLiveData.value != "" || clearPreviousSearch) {
|
||||
val previousItems = if (clearPreviousSearch) listOf() else searchedItems
|
||||
searchedItems = previousItems.plus(searchResult.items)
|
||||
itemsLiveData.value = searchedItems
|
||||
itemsLiveData.postValue(searchedItems)
|
||||
} else {
|
||||
items = items.plus(searchResult.items)
|
||||
itemsLiveData.value = items
|
||||
itemsLiveData.postValue(items)
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleLinkedItemAction(itemID: String, action: LinkedItemAction) {
|
||||
when (action) {
|
||||
LinkedItemAction.Delete -> {
|
||||
removeItemFromList(itemID)
|
||||
|
||||
viewModelScope.launch {
|
||||
networker.deleteLinkedItem(itemID)
|
||||
}
|
||||
}
|
||||
LinkedItemAction.Archive -> {
|
||||
removeItemFromList(itemID)
|
||||
viewModelScope.launch {
|
||||
networker.archiveLinkedItem(itemID)
|
||||
}
|
||||
}
|
||||
LinkedItemAction.Unarchive -> {
|
||||
removeItemFromList(itemID)
|
||||
viewModelScope.launch {
|
||||
networker.unarchiveLinkedItem(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeItemFromList(itemID: String) {
|
||||
itemsLiveData.value?.let {
|
||||
val newList = it.filter { item -> item.id != itemID }
|
||||
itemsLiveData.postValue(newList)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,3 +129,9 @@ class HomeViewModel @Inject constructor(
|
|||
return query
|
||||
}
|
||||
}
|
||||
|
||||
enum class LinkedItemAction {
|
||||
Delete,
|
||||
Archive,
|
||||
Unarchive
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package app.omnivore.omnivore.ui.home
|
||||
package app.omnivore.omnivore.ui.linkedItemViews
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -16,10 +13,13 @@ 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 coil.compose.rememberAsyncImagePainter
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LinkedItemCard(item: LinkedItem, onClickHandler: () -> Unit) {
|
||||
fun LinkedItemCard(item: LinkedItem, onClickHandler: () -> Unit, actionHandler: (LinkedItemAction) -> Unit) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
val publisherDisplayName = item.publisherDisplayName()
|
||||
|
||||
Column {
|
||||
|
|
@ -29,7 +29,11 @@ fun LinkedItemCard(item: LinkedItem, onClickHandler: () -> Unit) {
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
.clickable(onClick = onClickHandler)
|
||||
.combinedClickable(
|
||||
onClick = onClickHandler,
|
||||
onLongClick = { isMenuExpanded = true }
|
||||
)
|
||||
.background(if (isMenuExpanded) Color.LightGray else Color.Transparent)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
|
|
@ -75,5 +79,12 @@ fun LinkedItemCard(item: LinkedItem, onClickHandler: () -> Unit) {
|
|||
}
|
||||
|
||||
Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp)
|
||||
|
||||
LinkedItemContextMenu(
|
||||
isExpanded = isMenuExpanded,
|
||||
isArchived = item.isArchived,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
actionHandler = actionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package app.omnivore.omnivore.ui.linkedItemViews
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.List
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun LinkedItemContextMenu(
|
||||
isExpanded: Boolean,
|
||||
isArchived: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
actionHandler: (LinkedItemAction) -> Unit
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (isArchived) "Unarchive" else "Archive") },
|
||||
onClick = {
|
||||
val action = if (isArchived) LinkedItemAction.Unarchive else LinkedItemAction.Archive
|
||||
actionHandler(action)
|
||||
onDismiss()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.List, // TODO: use more appropriate icon
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Remove Item") },
|
||||
onClick = {
|
||||
actionHandler(LinkedItemAction.Delete)
|
||||
onDismiss()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.Delete,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -322,12 +322,13 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan
|
|||
p0.dismiss()
|
||||
return@OnPopupToolbarItemClickedListener true
|
||||
}
|
||||
// 2 -> {
|
||||
// Log.d("pdf", "user selected annotate action")
|
||||
2 -> {
|
||||
Log.d("pdf", "user selected annotate action")
|
||||
showAnnotationView("")
|
||||
// textSelectionController?.textSelection = null
|
||||
// p0.dismiss()
|
||||
// return@OnPopupToolbarItemClickedListener true
|
||||
// }
|
||||
p0.dismiss()
|
||||
return@OnPopupToolbarItemClickedListener true
|
||||
}
|
||||
3 -> {
|
||||
val text = textSelectionController?.textSelection?.text ?: ""
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
|
@ -349,7 +350,7 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan
|
|||
|
||||
p0.menuItems = listOf(
|
||||
PopupToolbarMenuItem(1, R.string.pdf_highlight_menu_action),
|
||||
// PopupToolbarMenuItem(2, R.string.annotate_menu_action),
|
||||
PopupToolbarMenuItem(2, R.string.annotate_menu_action),
|
||||
PopupToolbarMenuItem(3, R.string.pdf_highlight_copy),
|
||||
)
|
||||
}
|
||||
|
|
@ -432,6 +433,7 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan
|
|||
actionMode = null
|
||||
clickedHighlight = null
|
||||
clickedHighlightPosition = null
|
||||
textSelectionController?.textSelection = null
|
||||
viewModel.annotationUnderNoteEdit = null
|
||||
}
|
||||
|
||||
|
|
@ -443,8 +445,15 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan
|
|||
val annotationEditFragment = AnnotationEditFragment()
|
||||
annotationEditFragment.configure(
|
||||
onSave = { newNote ->
|
||||
clickedHighlight?.let { highlight ->
|
||||
viewModel.updateHighlightNote(highlight, newNote)
|
||||
if (clickedHighlight != null) {
|
||||
viewModel.updateHighlightNote(clickedHighlight!!, newNote)
|
||||
} else {
|
||||
pendingHighlightAnnotation?.let { annotation ->
|
||||
val quote = textSelectionController?.textSelection?.text ?: ""
|
||||
fragment.addAnnotationToPage(annotation, false) {
|
||||
viewModel.syncHighlightUpdates(annotation, quote, listOf(), newNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
resetHighlightTap()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class PDFReaderViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun syncHighlightUpdates(newAnnotation: Annotation, quote: String, overlapIds: List<String>) {
|
||||
fun syncHighlightUpdates(newAnnotation: Annotation, quote: String, overlapIds: List<String>, note: String? = null) {
|
||||
val itemID = pdfReaderParamsLiveData.value?.item?.id ?: return
|
||||
val highlightID = UUID.randomUUID().toString()
|
||||
val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8)
|
||||
|
|
@ -130,7 +130,7 @@ class PDFReaderViewModel @Inject constructor(
|
|||
}
|
||||
} else {
|
||||
val createHighlightInput = CreateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(null),
|
||||
annotation = Optional.presentIfNotNull(note),
|
||||
articleId = itemID,
|
||||
id = highlightID,
|
||||
patch = newAnnotation.toInstantJson(),
|
||||
|
|
@ -141,17 +141,16 @@ class PDFReaderViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
networker.createHighlight(createHighlightInput)
|
||||
}
|
||||
|
||||
if (note != null) {
|
||||
storeUpdatedNoteLocally(newAnnotation, note!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateHighlightNote(annotation: Annotation, note: String) {
|
||||
// Save the updated note locally
|
||||
val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject
|
||||
omnivoreHighlight?.put("editedNote", note)
|
||||
omnivoreHighlight?.let {
|
||||
Log.d("pdf", "setting custom data: $omnivoreHighlight")
|
||||
annotation.customData = JSONObject().put("omnivoreHighlight", it)
|
||||
}
|
||||
storeUpdatedNoteLocally(annotation, note)
|
||||
|
||||
// Sync update with data service
|
||||
viewModelScope.launch {
|
||||
|
|
@ -165,6 +164,15 @@ class PDFReaderViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun storeUpdatedNoteLocally(annotation: Annotation, note: String) {
|
||||
val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject
|
||||
omnivoreHighlight?.put("editedNote", note)
|
||||
omnivoreHighlight?.let {
|
||||
Log.d("pdf", "setting custom data: $omnivoreHighlight")
|
||||
annotation.customData = JSONObject().put("omnivoreHighlight", it)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteHighlight(annotation: Annotation) {
|
||||
val highlightID = pluckHighlightID(annotation) ?: return
|
||||
viewModelScope.launch {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ 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.*
|
||||
|
|
@ -28,6 +29,7 @@ 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 com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -39,6 +41,7 @@ import kotlin.math.roundToInt
|
|||
|
||||
@Composable
|
||||
fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewModel) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
var showWebPreferencesDialog by remember { mutableStateOf(false ) }
|
||||
|
||||
val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null)
|
||||
|
|
@ -94,12 +97,25 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod
|
|||
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
title = {},
|
||||
actions = {
|
||||
// Disabling menu until we implement local persistence
|
||||
// IconButton(onClick = { isMenuExpanded = true }) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Filled.Menu,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
IconButton(onClick = { showWebPreferencesDialog = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Settings,
|
||||
imageVector = Icons.Filled.Settings, // TODO: set a better icon
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
LinkedItemContextMenu(
|
||||
isExpanded = isMenuExpanded,
|
||||
isArchived = webReaderParams!!.item.isArchived,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
actionHandler = { webReaderViewModel.handleLinkedItemAction(webReaderParams!!.item.id, it) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -254,7 +270,7 @@ class OmnivoreWebView(context: Context) : WebView(context) {
|
|||
// Called when the user selects a contextual menu item
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.annotateHighlight -> {
|
||||
R.id.annotateHighlight, R.id.annotate -> {
|
||||
val script = "var event = new Event('annotate');document.dispatchEvent(event);"
|
||||
evaluateJavascript(script) {
|
||||
mode.finish()
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ import app.omnivore.omnivore.DatastoreKeys
|
|||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.ui.home.LinkedItemAction
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.*
|
||||
|
|
@ -62,6 +65,36 @@ class WebReaderViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleLinkedItemAction(itemID: String, action: LinkedItemAction) {
|
||||
when (action) {
|
||||
LinkedItemAction.Delete -> {
|
||||
viewModelScope.launch {
|
||||
networker.deleteLinkedItem(itemID)
|
||||
popToHomeView(itemID)
|
||||
}
|
||||
}
|
||||
LinkedItemAction.Archive -> {
|
||||
viewModelScope.launch {
|
||||
networker.archiveLinkedItem(itemID)
|
||||
popToHomeView(itemID)
|
||||
}
|
||||
}
|
||||
LinkedItemAction.Unarchive -> {
|
||||
viewModelScope.launch {
|
||||
networker.unarchiveLinkedItem(itemID)
|
||||
popToHomeView(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun popToHomeView(itemID: String) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
// TODO: pop to home
|
||||
Log.d("maxx", "should pop to home and remove item with ID: $itemID")
|
||||
}
|
||||
}
|
||||
|
||||
fun handleIncomingWebMessage(actionID: String, jsonString: String) {
|
||||
when (actionID) {
|
||||
"createHighlight" -> {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,13 @@ fun RootView(
|
|||
} else {
|
||||
WelcomeScreen(viewModel = loginViewModel)
|
||||
}
|
||||
|
||||
DisposableEffect(hasAuthToken) {
|
||||
if (hasAuthToken) {
|
||||
loginViewModel.registerUser()
|
||||
}
|
||||
onDispose {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import app.omnivore.omnivore.Routes
|
|||
import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||
import io.intercom.android.sdk.Intercom
|
||||
import io.intercom.android.sdk.IntercomSpace
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
|
|
@ -51,6 +53,11 @@ fun SettingsView(
|
|||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
LogoutButton { loginViewModel.logout() }
|
||||
Button(onClick = {
|
||||
Intercom.client().present(space = IntercomSpace.Messages)
|
||||
}) {
|
||||
Text(text = "Open Help Center")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/createHighlight"
|
||||
android:title="@string/pdf_highlight_menu_action"
|
||||
app:showAsAction="always">
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/copyPdfHighlight"
|
||||
android:title="@string/pdf_highlight_copy"
|
||||
app:showAsAction="always">
|
||||
</item>
|
||||
</menu>
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pspdfkit_license_key">unset</string>
|
||||
<string name="segment_write_key">unset</string>
|
||||
<string name="intercom_api_key">unset</string>
|
||||
<string name="intercom_app_id">unset</string>
|
||||
</resources>
|
||||
Loading…
Reference in a new issue