Edit notes on highlights/articles

This commit is contained in:
Jackson Harper
2023-05-30 12:08:55 +08:00
parent a4e6b00d05
commit 49903bf938
9 changed files with 154 additions and 173 deletions

View File

@ -53,6 +53,7 @@ fragment HighlightFields on Highlight {
patch
annotation
createdByMe
createdAt
updatedAt
sharedAt
}

View File

@ -139,8 +139,6 @@ 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(
type = createdHighlight.highlightFields.type.toString(),
highlightId = createdHighlight.highlightFields.id,
@ -150,8 +148,8 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
suffix = createdHighlight.highlightFields.suffix,
patch = createdHighlight.highlightFields.patch,
annotation = createdHighlight.highlightFields.annotation,
createdAt = null, // TODO: update gql query to get this
updatedAt = null, // TODO: fix updatedAtString?.let { LocalDate.parse(it) },
createdAt = createdHighlight.highlightFields.createdAt.toString(),
updatedAt = createdHighlight.highlightFields.updatedAt.toString(),
createdByMe = createdHighlight.highlightFields.createdByMe
)
} else {

View File

@ -51,8 +51,8 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
suffix = it.highlightFields.suffix,
patch = it.highlightFields.patch,
annotation = it.highlightFields.annotation,
createdAt = null, // TODO: update gql query to get this
updatedAt = null, //updatedAtString?.let { str -> LocalDate.parse(str) }, TODO: fix date parsing
createdAt = it.highlightFields.createdAt as String?,
updatedAt = it.highlightFields.updatedAt as String?,
createdByMe = it.highlightFields.createdByMe
)
}

View File

@ -64,7 +64,7 @@ suspend fun Networker.search(
savedItemLabelId = label.labelFields.id,
name = label.labelFields.name,
color = label.labelFields.color,
createdAt = null,
createdAt = label.labelFields.createdAt as String?,
labelDescription = null
)
},
@ -81,7 +81,7 @@ suspend fun Networker.search(
shortId = highlight.highlightFields.shortId,
suffix = highlight.highlightFields.suffix,
updatedAt = highlight.highlightFields.updatedAt as String?,
createdAt = null,
createdAt = highlight.highlightFields.createdAt as String?,
)
}
)

View File

@ -13,7 +13,7 @@ data class Highlight(
val type: String,
var annotation: String?,
val createdAt: String?,
val createdByMe: Boolean,
val createdByMe: Boolean = true,
val markedForDeletion: Boolean = false,
var patch: String?,
var prefix: String?,
@ -22,10 +22,6 @@ data class Highlight(
var shortId: String,
val suffix: String?,
val updatedAt: String?
// has many SavedItemLabels (inverse: labels have many highlights)
// has one savedItem (inverse: savedItem has many highlights
// has a UserProfile (no inverse)
)
@Entity(

View File

@ -3,14 +3,10 @@ package app.omnivore.omnivore.ui.notebook
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.TextFieldColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
@ -35,9 +31,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -77,7 +71,7 @@ fun notebookMD(notes: List<Highlight>, highlights: List<Highlight>): String {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
fun NotebookView(savedItemId: String, viewModel: NotebookViewModel, onEditArticleNotes: () -> Unit) {
fun NotebookView(savedItemId: String, viewModel: NotebookViewModel, onEditNote: (note: Highlight?) -> Unit) {
var isMenuOpen by remember {
mutableStateOf(false)
}
@ -142,8 +136,8 @@ fun NotebookView(savedItemId: String, viewModel: NotebookViewModel, onEditArticl
.padding(top = paddingValues.calculateTopPadding())
) {
savedItem.value?.let {
ArticleNotes(viewModel, it, onEditArticleNotes)
HighlightsList(it)
ArticleNotes(viewModel, it, onEditNote)
HighlightsList(it, onEditNote)
}
}
}
@ -202,7 +196,7 @@ fun EditNoteModal(initialValue: String?, onDismiss: (save: Boolean, text: String
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
fun ArticleNotes(viewModel: NotebookViewModel, item: SavedItemWithLabelsAndHighlights, onEditArticleNotes: () -> Unit) {
fun ArticleNotes(viewModel: NotebookViewModel, item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Highlight?) -> Unit) {
val notes = item.highlights?.filter { it.type == "NOTE" } ?: listOf()
Column(modifier = Modifier
@ -218,12 +212,13 @@ fun ArticleNotes(viewModel: NotebookViewModel, item: SavedItemWithLabelsAndHighl
fontSize = 14.sp,
style = TextStyle(lineHeight = 18.sp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
onClick = { onEditNote(note) }
)
}
if (notes.isEmpty()) {
Button(
onClick = {
onEditArticleNotes()
onEditNote(null)
},
modifier = Modifier
.padding(0.dp, end = 15.dp)
@ -248,7 +243,7 @@ fun ArticleNotes(viewModel: NotebookViewModel, item: SavedItemWithLabelsAndHighl
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HighlightsList(item: SavedItemWithLabelsAndHighlights) {
fun HighlightsList(item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Highlight?) -> Unit) {
val highlights = item.highlights?.filter { it.type == "HIGHLIGHT" } ?: listOf()
val yellowColor = colorResource(R.color.cta_yellow)
@ -265,98 +260,107 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights) {
Text("Highlights")
Divider(modifier = Modifier.padding(bottom= 10.dp))
highlights.forEach { highlight ->
var isMenuOpen by remember { mutableStateOf(false) }
var isMenuOpen by remember { mutableStateOf(false) }
Row(modifier = Modifier
.fillMaxWidth()
.align(Alignment.End)
.padding(0.dp)
) {
Spacer(Modifier.weight(1f))
Box {
IconButton(onClick = { isMenuOpen = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null
)
}
if (isMenuOpen) {
DropdownMenu(
expanded = isMenuOpen,
onDismissRequest = { isMenuOpen = false }
) {
DropdownMenuItem(
text = { Text("Copy") },
onClick = {
val clip = ClipData.newPlainText("highlight", highlight.quote)
clipboard?.let {
clipboard?.setPrimaryClip(clip)
} ?: run {
coroutineScope.launch {
snackBarHostState
.showSnackbar("Highlight copied")
}
}
isMenuOpen = false
}
)
}
}
}
}
highlight.quote?.let {
Row(modifier = Modifier
.padding(start = 2.dp, end = 15.dp)
.fillMaxWidth()
.drawWithCache {
onDrawWithContent {
// draw behind the content the vertical line on the left
drawLine(
color = yellowColor,
start = Offset.Zero,
end = Offset(0f, this.size.height),
strokeWidth = 10f
)
// draw the content
drawContent()
}
}) {
MarkdownText(
modifier = Modifier
.padding(start = 15.dp, end = 15.dp),
markdown = it,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
Row(modifier = Modifier
.fillMaxWidth()
.align(Alignment.End)
.padding(0.dp)
) {
Spacer(Modifier.weight(1f))
Box {
IconButton(onClick = { isMenuOpen = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null
)
}
if (isMenuOpen) {
DropdownMenu(
expanded = isMenuOpen,
onDismissRequest = { isMenuOpen = false }
) {
DropdownMenuItem(
text = { Text("Copy") },
onClick = {
val clip = ClipData.newPlainText("highlight", highlight.quote)
clipboard?.let {
clipboard?.setPrimaryClip(clip)
} ?: run {
coroutineScope.launch {
snackBarHostState
.showSnackbar("Highlight copied")
}
}
isMenuOpen = false
}
)
}
}
}
highlight.annotation?.let {
}
highlight.quote?.let {
Row(modifier = Modifier
.padding(start = 2.dp, end = 15.dp)
.fillMaxWidth()
.drawWithCache {
onDrawWithContent {
// draw behind the content the vertical line on the left
drawLine(
color = yellowColor,
start = Offset.Zero,
end = Offset(0f, this.size.height),
strokeWidth = 10f
)
// draw the content
drawContent()
}
}) {
MarkdownText(
// modifier = Modifier.padding(paddingValues),
modifier = Modifier
.padding(start = 15.dp, end = 15.dp),
markdown = it,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
} ?: run {
// Surface(
// modifier = Modifier
// .padding(0.dp, end = 15.dp, top = 15.dp, bottom = 30.dp)
// .fillMaxWidth(),
// shape = androidx.compose.material.MaterialTheme.shapes.medium,
// color = MaterialTheme.colorScheme.surfaceVariant
// ) {
// Row {
// Text(
// text = "Add Notes...",
// style = androidx.compose.material.MaterialTheme.typography.subtitle2,
// modifier = Modifier.padding(vertical = 10.dp, horizontal = 10.dp)
// )
// }
// }
)
}
}
highlight.annotation?.let {
MarkdownText(
// modifier = Modifier.padding(paddingValues),
markdown = it,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
onClick = { onEditNote(highlight) },
modifier = Modifier
.padding(top = 15.dp),
)
} ?: run {
Button(
onClick = {
onEditNote(highlight)
},
modifier = Modifier
.padding(0.dp, top = 15.dp, end = 15.dp)
.fillMaxWidth(),
shape = androidx.compose.material.MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Text(
text = "Add Note...",
style = androidx.compose.material.MaterialTheme.typography.subtitle2,
modifier = Modifier
.padding(vertical = 2.dp, horizontal = 0.dp),
)
Spacer(Modifier.weight(1f))
}
}
}
if (highlights.isEmpty()) {
Text(

View File

@ -1,16 +1,17 @@
package app.omnivore.omnivore.ui.notebook
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import androidx.lifecycle.*
import app.omnivore.omnivore.DatastoreRepository
import app.omnivore.omnivore.dataService.DataService
import app.omnivore.omnivore.dataService.createNoteHighlight
import app.omnivore.omnivore.networking.Networker
import app.omnivore.omnivore.persistence.entities.Highlight
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.ui.library.SavedItemViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
@ -19,22 +20,24 @@ class NotebookViewModel @Inject constructor(
private val dataService: DataService,
private val datastoreRepo: DatastoreRepository
): ViewModel() {
var highlightUnderEdit: Highlight? = null
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights> {
return dataService.db.savedItemDao().getLibraryItemById(savedItemId)
}
suspend fun addArticleNote(savedItemId: String, note: String): Boolean {
val item = dataService.db.savedItemDao().getById(savedItemId)
println("this is an item: $item")
return item?.let { item ->
println("this is an item: $item")
val noteHighlight = item.highlights.firstOrNull { it.type == "NOTE" }
noteHighlight?.let {
dataService.db.highlightDao().updateNote(highlightId = noteHighlight.highlightId, note = note)
} ?: run {
dataService.createNoteHighlight(savedItemId, note)
suspend fun addArticleNote(savedItemId: String, note: String) {
withContext(Dispatchers.IO) {
val item = dataService.db.savedItemDao().getById(savedItemId)
item?.let { item ->
val noteHighlight = item.highlights.firstOrNull { it.type == "NOTE" }
noteHighlight?.let {
dataService.db.highlightDao()
.updateNote(highlightId = noteHighlight.highlightId, note = note)
} ?: run {
dataService.createNoteHighlight(savedItemId, note)
}
}
return true
} ?: false
}
}
}

View File

@ -32,8 +32,12 @@ import java.util.*
@Composable
fun WebReader(
styledContent: String,
webReaderViewModel: WebReaderViewModel
webReaderViewModel: WebReaderViewModel,
currentTheme: Themes?
) {
val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState()
val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value }
val javascriptActionLoopUUID: UUID by webReaderViewModel
.javascriptActionLoopUUIDLiveData
.observeAsState(UUID.randomUUID())
@ -55,15 +59,11 @@ fun WebReader(
settings.allowFileAccess = true
settings.domStorageEnabled = true
alpha = 0.0f
alpha = 1.0f
viewModel?.showNavBar()
setBackgroundColor(0xff0000);
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
viewModel?.showNavBar()
view?.animate()?.alpha(1.0f)?.duration = 200
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?

View File

@ -3,7 +3,6 @@ package app.omnivore.omnivore.ui.reader
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.activity.compose.setContent
@ -15,11 +14,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.*
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
@ -50,7 +46,6 @@ import androidx.compose.material3.Button
import androidx.compose.ui.platform.LocalContext
import app.omnivore.omnivore.ui.notebook.EditNoteModal
@AndroidEntryPoint
class WebReaderLoadingContainerActivity: ComponentActivity() {
val viewModel: WebReaderViewModel by viewModels()
@ -116,7 +111,7 @@ enum class BottomSheetState(
NONE(),
PREFERENCES(),
NOTEBOOK(),
ADDNOTE(),
EDITNOTE(),
HIGHLIGHTNOTE(),
LABELS(),
LINK()
@ -129,6 +124,8 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
onLibraryIconTap: (() -> Unit)? = null,
webReaderViewModel: WebReaderViewModel,
notebookViewModel: NotebookViewModel) {
val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState()
val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value }
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val bottomSheetState: BottomSheetState? by webReaderViewModel.bottomSheetStateLiveData.observeAsState(BottomSheetState.NONE)
@ -155,7 +152,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
val modalBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
skipHalfExpanded = bottomSheetState == BottomSheetState.ADDNOTE,
skipHalfExpanded = bottomSheetState == BottomSheetState.EDITNOTE,
confirmValueChange = {
if (it == ModalBottomSheetValue.Hidden) {
webReaderViewModel.resetBottomSheet()
@ -164,6 +161,11 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
}
)
val showMenu = {
coroutineScope.launch {
modalBottomSheetState.show()
}
}
when (bottomSheetState) {
BottomSheetState.PREFERENCES -> {
@ -173,35 +175,10 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
}
}
}
BottomSheetState.NOTEBOOK -> {
coroutineScope.launch {
modalBottomSheetState.show()
}
}
BottomSheetState.ADDNOTE -> {
coroutineScope.launch {
modalBottomSheetState.show()
}
}
BottomSheetState.HIGHLIGHTNOTE -> {
coroutineScope.launch {
modalBottomSheetState.show()
}
}
BottomSheetState.LABELS -> {
coroutineScope.launch {
modalBottomSheetState.show()
}
}
BottomSheetState.LINK -> {
coroutineScope.launch {
modalBottomSheetState.show()
}
}
BottomSheetState.NONE -> {
coroutineScope.launch {
modalBottomSheetState.hide()
}
BottomSheetState.NOTEBOOK, BottomSheetState.EDITNOTE,
BottomSheetState.HIGHLIGHTNOTE, BottomSheetState.LABELS,
BottomSheetState.LINK, -> {
showMenu()
}
else -> {
coroutineScope.launch {
@ -225,16 +202,17 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
BottomSheetState.NOTEBOOK -> {
webReaderParams?.let { params ->
BottomSheetUI(title = "Notebook") {
NotebookView(savedItemId = params.item.savedItemId, viewModel = notebookViewModel, onEditArticleNotes = {
webReaderViewModel.setBottomSheet(BottomSheetState.ADDNOTE)
NotebookView(savedItemId = params.item.savedItemId, viewModel = notebookViewModel, onEditNote = {
notebookViewModel.highlightUnderEdit = it
webReaderViewModel.setBottomSheet(BottomSheetState.EDITNOTE)
})
}
}
}
BottomSheetState.ADDNOTE -> {
BottomSheetState.EDITNOTE -> {
webReaderParams?.let { params ->
EditNoteModal(
initialValue = null,
initialValue = notebookViewModel.highlightUnderEdit?.annotation,
onDismiss = { save, note ->
if (save && note != null) {
coroutineScope.launch {
@ -318,7 +296,8 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
if (styledContent != null) {
WebReader(
styledContent = styledContent,
webReaderViewModel = webReaderViewModel
webReaderViewModel = webReaderViewModel,
currentTheme = currentTheme,
)
}