First pass at Android Notebooks, most parts in place, lots of styling needed

This commit is contained in:
Jackson Harper
2023-05-03 18:53:01 +08:00
parent 4ae229da1a
commit 51b5c578f1
11 changed files with 405 additions and 3 deletions

View File

@ -148,6 +148,8 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation 'com.github.jeziellago:compose-markdown:0.3.3'
}
apollo {

View File

@ -57,5 +57,10 @@
android:exported="true"
android:theme="@style/Theme.Omnivore"/>
<activity
android:name=".ui.notebook.NotebookActivity"
android:exported="true"
android:theme="@style/Theme.Omnivore"/>
</application>
</manifest>

View File

@ -7,4 +7,5 @@ sealed class Routes(val route: String) {
object Documentation: Routes("Documentation")
object PrivacyPolicy: Routes("PrivacyPolicy")
object TermsAndConditions: Routes("TermsAndConditions")
object Notebook: Routes("Notebook")
}

View File

@ -162,6 +162,22 @@ interface SavedItemDao {
)
fun getLibraryLiveDataSortedByRecentlyPublished(archiveFilter: Int): LiveData<List<SavedItemCardDataWithLabels>>
@Transaction
@Query(
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
"FROM SavedItem " +
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
"WHERE SavedItem.savedItemId != :savedItemId " +
"GROUP BY SavedItem.savedItemId "
)
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights>
@Transaction
@Query(
"SELECT ${SavedItemQueryConstants.libraryColumns} " +

View File

@ -0,0 +1,327 @@
package app.omnivore.omnivore.ui.notebook
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.ContextMenu
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.LocalContentColor
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import app.omnivore.omnivore.MainActivity
import app.omnivore.omnivore.R
import app.omnivore.omnivore.ui.components.WebReaderLabelsSelectionSheet
import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import dagger.hilt.android.AndroidEntryPoint
import kotlin.math.roundToInt
import androidx.navigation.compose.rememberNavController
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.ui.components.LabelChipColors
import app.omnivore.omnivore.ui.library.SavedItemFilter
import app.omnivore.omnivore.ui.library.SearchField
import app.omnivore.omnivore.ui.library.SearchViewContent
import app.omnivore.omnivore.ui.library.TypeaheadSearchViewContent
import app.omnivore.omnivore.ui.reader.WebReaderViewModel
import app.omnivore.omnivore.ui.theme.md_theme_dark_outline
import dev.jeziellago.compose.markdowntext.MarkdownText
@AndroidEntryPoint
class NotebookActivity: ComponentActivity() {
val viewModel: NotebookViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val savedItemId = intent.getStringExtra("SAVED_ITEM_ID")
setContent {
val systemUiController = rememberSystemUiController()
val useDarkIcons = !isSystemInDarkTheme()
DisposableEffect(systemUiController, useDarkIcons) {
systemUiController.setSystemBarsColor(
color = Color.Black,
darkIcons = false
)
onDispose {}
}
OmnivoreTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Black)
) {
savedItemId?.let {
NotebookView(
savedItemId = savedItemId,
viewModel = viewModel
)
}
}
}
}
}
// // animate the view up when keyboard appears
// WindowCompat.setDecorFitsSystemWindows(window, false)
// val rootView = findViewById<View>(android.R.id.content).rootView
// ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
// val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
// rootView.setPadding(0, 0, 0, imeHeight)
// insets
// }
// }
private fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
this.startActivity(intent)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NotebookView(savedItemId: String, viewModel: NotebookViewModel) {
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val savedItem = viewModel.getLibraryItemById(savedItemId).observeAsState()
// var isMenuExpanded by remember { mutableStateOf(false) }
// var showWebPreferencesDialog by remember { mutableStateOf(false ) }
val noteMarkdown = "This is some *markdown* for a note."
OmnivoreTheme() {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Notebook") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
navigationIcon = {
IconButton(onClick = {
onBackPressedDispatcher?.onBackPressed()
}) {
Icon(
imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack,
modifier = Modifier,
contentDescription = "Back"
)
}
},
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
) {
savedItem.value?.let {
ArticleNotes(it)
HighlightsList(it)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ArticleNotes(item: SavedItemWithLabelsAndHighlights) {
val notes = item.highlights?.filter { it.type == "NOTE" } ?: listOf()
val listState = rememberLazyListState()
Column(modifier = Modifier
.fillMaxWidth()
.padding(start = 15.dp)
.padding(top = 40.dp, bottom = 10.dp)
) {
Text("Article Notes")
Divider(modifier = Modifier.padding(bottom= 15.dp))
Surface(
modifier = Modifier
.padding(0.dp, end = 15.dp)
.fillMaxWidth(),
shape = androidx.compose.material.MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surfaceVariant
) {
LazyColumn(
state = listState,
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
// modifier = Modifier
// .fillMaxSize()
) {
items(notes) { note ->
MarkdownText(
// modifier = Modifier.padding(paddingValues),
markdown = note.annotation ?: "",
fontSize = 12.sp,
)
}
}
if (notes.isEmpty()) {
Text(
text = "Add Notes...",
style = androidx.compose.material.MaterialTheme.typography.subtitle2,
modifier = Modifier.padding(vertical = 10.dp, horizontal = 10.dp)
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HighlightsList(item: SavedItemWithLabelsAndHighlights) {
val highlights = item.highlights?.filter { it.type == "HIGHLIGHT" } ?: listOf()
val listState = rememberLazyListState()
val yellowColor = colorResource(R.color.cta_yellow)
Column(modifier = Modifier
.fillMaxWidth()
.padding(start = 15.dp)
.padding(top = 40.dp)
) {
Text("Highlights")
Divider(modifier = Modifier.padding(bottom= 10.dp))
LazyColumn(
state = listState,
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
) {
items(highlights) { highlight ->
var isMenuOpen by remember { mutableStateOf(false) }
Row(modifier = Modifier
.fillMaxWidth()
.align(Alignment.End)) {
IconButton(onClick = { isMenuOpen = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null
)
}
DropdownMenu(
expanded = isMenuOpen,
onDismissRequest = { isMenuOpen = false }
) {
DropdownMenuItem(
text = { Text("Copy") },
onClick = {
// actionHandler(it)
// onDismiss()
}
)
}
}
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 = 12.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
}
highlight.annotation?.let {
MarkdownText(
// modifier = Modifier.padding(paddingValues),
markdown = it,
fontSize = 12.sp,
)
} ?: 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)
)
}
}
}
}
}
if (highlights.isEmpty()) {
Text(
text = "You have not added any highlights to this page.",
style = androidx.compose.material.MaterialTheme.typography.subtitle2,
modifier = Modifier.padding(vertical = 10.dp, horizontal = 10.dp)
)
}
}
}

View File

@ -0,0 +1,23 @@
package app.omnivore.omnivore.ui.notebook
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import app.omnivore.omnivore.DatastoreRepository
import app.omnivore.omnivore.dataService.DataService
import app.omnivore.omnivore.networking.Networker
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.ui.library.SavedItemViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class NotebookViewModel @Inject constructor(
private val networker: Networker,
private val dataService: DataService,
private val datastoreRepo: DatastoreRepository
): ViewModel() {
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights> {
return dataService.db.savedItemDao().getLibraryItemById(savedItemId)
}
}

View File

@ -26,6 +26,7 @@ import androidx.compose.runtime.livedata.observeAsState
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.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@ -33,8 +34,6 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import app.omnivore.omnivore.MainActivity
import app.omnivore.omnivore.R
import app.omnivore.omnivore.ui.components.WebReaderLabelsSelectionSheet
@ -44,6 +43,8 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import dagger.hilt.android.AndroidEntryPoint
import kotlin.math.roundToInt
import androidx.navigation.compose.rememberNavController
import app.omnivore.omnivore.Routes
import app.omnivore.omnivore.ui.notebook.NotebookActivity
@AndroidEntryPoint
@ -55,7 +56,6 @@ class WebReaderLoadingContainerActivity: ComponentActivity() {
val requestID = intent.getStringExtra("SAVED_ITEM_REQUEST_ID")
val slug = intent.getStringExtra("SAVED_ITEM_SLUG")
setContent {
val systemUiController = rememberSystemUiController()
val useDarkIcons = !isSystemInDarkTheme()
@ -122,6 +122,8 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, o
webReaderViewModel.maxToolbarHeightPx = with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() }
webReaderViewModel.loadItem(slug = slug, requestID = requestID)
val context = LocalContext.current
val styledContent = webReaderParams?.let {
val webReaderContent = WebReaderContent(
preferences = webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()),
@ -171,6 +173,18 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, o
)
}
}
webReaderParams?.let {
IconButton(onClick = {
val intent = Intent(context, NotebookActivity::class.java)
intent.putExtra("SAVED_ITEM_ID", it.item.savedItemId)
context.startActivity(intent)
}) {
Icon(
painter = painterResource(id = R.drawable.notebook),
contentDescription = null
)
}
}
IconButton(onClick = { showWebPreferencesDialog = true }) {
Icon(
painter = painterResource(id = R.drawable.format_letter_case),

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M184,112a8,8 0,0 1,-8 8L112,120a8,8 0,0 1,0 -16h64A8,8 0,0 1,184 112ZM176,136L112,136a8,8 0,0 0,0 16h64a8,8 0,0 0,0 -16ZM224,48L224,208a16,16 0,0 1,-16 16L48,224a16,16 0,0 1,-16 -16L32,48A16,16 0,0 1,48 32L208,32A16,16 0,0 1,224 48ZM48,208L72,208L72,48L48,48ZM208,208L208,48L88,48L88,208L208,208Z"
android:fillColor="#000000"/>
</vector>

View File

@ -10,4 +10,8 @@
<color name="gray_EBEBEB">#EBEBEB</color>
<color name="yellow_E6E4BF">#E6E4BF</color>
<color name="cta_yellow">#F8D457</color>
<!-- <color name="F8F8F8">#F8F8F8</color>-->
<!-- <color name="2E2E2B">#2E2E2B</color>-->
</resources>

View File

@ -13,6 +13,7 @@ dependencyResolutionManagement {
maven {
url = uri("https://customers.pspdfkit.com/maven")
}
maven { url 'https://jitpack.io' }
}
}
rootProject.name = "Omnivore"