Merge pull request #2441 from omnivore-app/fix/android-create-label-crash

Fix crash when creating labels during SetLabels operation
This commit is contained in:
Jackson Harper
2023-07-01 00:18:43 +08:00
committed by GitHub
11 changed files with 148 additions and 31 deletions

View File

@ -17,8 +17,8 @@ android {
applicationId "app.omnivore.omnivore"
minSdk 26
targetSdk 33
versionCode 87
versionName "0.0.87"
versionCode 89
versionName "0.0.89"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

File diff suppressed because one or more lines are too long

View File

@ -14,6 +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.components.LabelsViewModel
import app.omnivore.omnivore.ui.library.LibraryViewModel
import app.omnivore.omnivore.ui.library.SearchViewModel
import app.omnivore.omnivore.ui.root.RootView
@ -35,6 +36,7 @@ class MainActivity : ComponentActivity() {
val libraryViewModel: LibraryViewModel by viewModels()
val settingsViewModel: SettingsViewModel by viewModels()
val searchViewModel: SearchViewModel by viewModels()
val labelsViewModel: LabelsViewModel by viewModels()
val context = this
@ -55,7 +57,7 @@ class MainActivity : ComponentActivity() {
.fillMaxSize()
.background(color = Color.Black)
) {
RootView(loginViewModel, searchViewModel, libraryViewModel, settingsViewModel)
RootView(loginViewModel, searchViewModel, libraryViewModel, settingsViewModel, labelsViewModel)
}
}
}

View File

@ -2,6 +2,7 @@ package app.omnivore.omnivore.persistence.entities
import androidx.lifecycle.LiveData
import androidx.room.*
import app.omnivore.omnivore.models.ServerSyncStatus
@Entity
data class SavedItemLabel(
@ -21,6 +22,14 @@ interface SavedItemLabelDao {
@Transaction
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
@Transaction
@Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId")
fun updateTempLabel(tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED)
@Transaction
@Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC")
fun namedLabels(names: List<String>): List<SavedItemLabel>
}
@Entity(

View File

@ -217,6 +217,14 @@ class LabelChipView(label: SavedItemLabel) : Chip(label.name) {
val label = label
}
fun findOrCreateLabel(labelsViewModel: LabelsViewModel, labels: List<SavedItemLabel>, name: TextFieldValue): SavedItemLabel {
val found = labels.find { it.name == name.text }
if (found != null) {
return found
}
return labelsViewModel.createNewSavedItemLabelWithTemp(name.text, LabelSwatchHelper.random())
}
@Composable
@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class,
ExperimentalMaterial3Api::class
@ -225,6 +233,7 @@ fun LabelsSelectionSheetContent(
isLibraryMode: Boolean,
labels: List<SavedItemLabel>,
initialSelectedLabels: List<SavedItemLabel>,
labelsViewModel: LabelsViewModel,
onCancel: () -> Unit,
onSave: (List<SavedItemLabel>) -> Unit,
onCreateLabel: (String, String) -> Unit
@ -253,20 +262,6 @@ fun LabelsSelectionSheetContent(
val titleText = if (isLibraryMode) "Filter by Label" else "Set Labels"
val findOrCreateLabel: (name: TextFieldValue) -> SavedItemLabel = { name ->
val found = labels.find { it.name == name.text }
found
?: SavedItemLabel(
savedItemLabelId = "",
name = name.text,
labelDescription = "",
color = LabelSwatchHelper.random(),
createdAt = LocalDate.now().atStartOfDay().atOffset(ZoneOffset.UTC).format(
DateTimeFormatter.ISO_DATE_TIME),
serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
)
}
Surface(
modifier = Modifier
.fillMaxSize()
@ -307,7 +302,7 @@ fun LabelsSelectionSheetContent(
LabelChipView(it)
} ?: null
} else {
LabelChipView(findOrCreateLabel(it))
LabelChipView(findOrCreateLabel(labelsViewModel = labelsViewModel, labels = labels, name = it))
}
},
chipLeadingIcon = { chip -> CircleIcon(colorHex = chip.label.color) },
@ -346,7 +341,7 @@ fun LabelsSelectionSheetContent(
modifier = Modifier
.fillMaxWidth()
.clickable {
val label = findOrCreateLabel(filterTextValue)
val label = findOrCreateLabel(labelsViewModel = labelsViewModel, labels = labels, name = filterTextValue)
state.addChip(LabelChipView(label))
filterTextValue = TextFieldValue()
}

View File

@ -0,0 +1,76 @@
package app.omnivore.omnivore.ui.components
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.*
import app.omnivore.omnivore.DatastoreKeys
import app.omnivore.omnivore.DatastoreRepository
import app.omnivore.omnivore.dataService.*
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
import app.omnivore.omnivore.models.ServerSyncStatus
import app.omnivore.omnivore.networking.*
import app.omnivore.omnivore.persistence.entities.SavedItem
import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
import app.omnivore.omnivore.ui.components.LabelSwatchHelper
import app.omnivore.omnivore.ui.library.SavedItemAction
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
import javax.inject.Inject
@HiltViewModel
class LabelsViewModel @Inject constructor(
private val datastoreRepo: DatastoreRepository,
private val dataService: DataService,
private val networker: Networker
): ViewModel() {
fun createNewSavedItemLabelWithTemp(labelName: String, hexColorValue: String): SavedItemLabel {
val tempId = UUID.randomUUID().toString()
val res = SavedItemLabel(
savedItemLabelId = tempId,
name = labelName,
labelDescription = "",
color = hexColorValue,
createdAt = LocalDate.now().atStartOfDay().atOffset(ZoneOffset.UTC).format(
DateTimeFormatter.ISO_DATE_TIME
),
serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
)
viewModelScope.launch {
withContext(Dispatchers.IO) {
dataService.db.savedItemLabelDao().insertAll(listOf(res))
val newLabel = networker.createNewLabel(CreateLabelInput(color = presentIfNotNull(res.color), name = res.name))
if (newLabel != null) {
try {
dataService.db.savedItemLabelDao().updateTempLabel(tempId, newLabel.id)
} catch (e: Exception) {
Log.d("EXCEPTION: ", e.toString())
}
}
}
}
return res
}
}

View File

@ -37,6 +37,7 @@ import app.omnivore.omnivore.Routes
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.ui.components.LabelsSelectionSheetContent
import app.omnivore.omnivore.ui.components.LabelsViewModel
import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard
import app.omnivore.omnivore.ui.reader.PDFReaderActivity
import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity
@ -48,6 +49,7 @@ import kotlinx.coroutines.launch
@Composable
fun LibraryView(
libraryViewModel: LibraryViewModel,
labelsViewModel: LabelsViewModel,
navController: NavHostController
) {
val scaffoldState: ScaffoldState = rememberScaffoldState()
@ -80,7 +82,7 @@ fun LibraryView(
sheetBackgroundColor = Color.Transparent,
sheetState = modalBottomSheetState,
sheetContent = {
BottomSheetContent(libraryViewModel)
BottomSheetContent(libraryViewModel, labelsViewModel)
Spacer(modifier = Modifier.weight(1.0F))
}
) {
@ -104,7 +106,7 @@ fun LibraryView(
}
@Composable
fun BottomSheetContent(libraryViewModel: LibraryViewModel) {
fun BottomSheetContent(libraryViewModel: LibraryViewModel, labelsViewModel: LabelsViewModel) {
val showLabelsSelectionSheet: Boolean by libraryViewModel.showLabelsSelectionSheetLiveData.observeAsState(false)
val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit()
val labels: List<SavedItemLabel> by libraryViewModel.savedItemLabelsLiveData.observeAsState(listOf())
@ -114,6 +116,7 @@ fun BottomSheetContent(libraryViewModel: LibraryViewModel) {
if (currentSavedItemData != null) {
LabelsSelectionSheetContent(
labels = labels,
labelsViewModel = labelsViewModel,
initialSelectedLabels = currentSavedItemData.labels,
onCancel = {
libraryViewModel.showLabelsSelectionSheetLiveData.value = false
@ -137,6 +140,7 @@ fun BottomSheetContent(libraryViewModel: LibraryViewModel) {
} else { // Is used in library mode
LabelsSelectionSheetContent(
labels = labels,
labelsViewModel = labelsViewModel,
initialSelectedLabels = libraryViewModel.activeLabelsLiveData.value ?: listOf(),
onCancel = { libraryViewModel.showLabelsSelectionSheetLiveData.value = false },
isLibraryMode = true,

View File

@ -44,12 +44,14 @@ import kotlinx.coroutines.launch
import kotlin.math.roundToInt
import androidx.compose.material3.Button
import androidx.compose.ui.platform.LocalContext
import app.omnivore.omnivore.ui.components.LabelsViewModel
import app.omnivore.omnivore.ui.notebook.EditNoteModal
@AndroidEntryPoint
class WebReaderLoadingContainerActivity: ComponentActivity() {
val viewModel: WebReaderViewModel by viewModels()
val notebookViewModel: NotebookViewModel by viewModels()
private val notebookViewModel: NotebookViewModel by viewModels()
private val labelsViewModel: LabelsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -84,6 +86,7 @@ class WebReaderLoadingContainerActivity: ComponentActivity() {
onLibraryIconTap = if (requestID != null) { { startMainActivity() } } else null,
webReaderViewModel = viewModel,
notebookViewModel = notebookViewModel,
labelsViewModel = labelsViewModel,
)
}
}
@ -123,7 +126,8 @@ enum class BottomSheetState(
fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
onLibraryIconTap: (() -> Unit)? = null,
webReaderViewModel: WebReaderViewModel,
notebookViewModel: NotebookViewModel) {
notebookViewModel: NotebookViewModel,
labelsViewModel: LabelsViewModel) {
val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState()
val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value }
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
@ -253,6 +257,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
BottomSheetUI(title = "Notebook") {
LabelsSelectionSheetContent(
labels = labels,
labelsViewModel = labelsViewModel,
initialSelectedLabels = webReaderParams?.labels ?: listOf(),
onCancel = {
coroutineScope.launch {

View File

@ -15,16 +15,21 @@ import app.omnivore.omnivore.DatastoreRepository
import app.omnivore.omnivore.dataService.*
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
import app.omnivore.omnivore.models.ServerSyncStatus
import app.omnivore.omnivore.networking.*
import app.omnivore.omnivore.persistence.entities.SavedItem
import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
import app.omnivore.omnivore.ui.components.LabelSwatchHelper
import app.omnivore.omnivore.ui.library.SavedItemAction
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
import javax.inject.Inject
@ -446,11 +451,27 @@ class WebReaderViewModel @Inject constructor(
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val input = SetLabelsInput(labelIds = labels.map { it.savedItemLabelId }, pageId = savedItemID)
val namedLabels = dataService.db.savedItemLabelDao().namedLabels(labels.map { it.name })
namedLabels.filter { it.serverSyncStatus != ServerSyncStatus.IS_SYNCED.rawValue }.mapNotNull {
val result = networker.createNewLabel(CreateLabelInput(color = presentIfNotNull(it.color), name = it.name))
result?.let { it1 ->
SavedItemLabel(
savedItemLabelId = it1.id,
name = result.name,
color = result.color,
createdAt = result.createdAt.toString(),
labelDescription = result.description,
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
)
}
}
val input = SetLabelsInput(labelIds = namedLabels.map { it.savedItemLabelId }, pageId = savedItemID)
val networkResult = networker.updateLabelsForSavedItem(input)
// TODO: assign a server sync status to these
val crossRefs = labels.map {
val crossRefs = namedLabels.map {
SavedItemAndSavedItemLabelCrossRef(
savedItemLabelId = it.savedItemLabelId,
savedItemId = savedItemID

View File

@ -18,6 +18,7 @@ import app.omnivore.omnivore.Routes
import app.omnivore.omnivore.dataService.DataService
import app.omnivore.omnivore.ui.auth.LoginViewModel
import app.omnivore.omnivore.ui.auth.WelcomeScreen
import app.omnivore.omnivore.ui.components.LabelsViewModel
import app.omnivore.omnivore.ui.library.LibraryView
import app.omnivore.omnivore.ui.library.SearchView
import app.omnivore.omnivore.ui.library.LibraryViewModel
@ -31,7 +32,8 @@ fun RootView(
loginViewModel: LoginViewModel,
searchViewModel: SearchViewModel,
libraryViewModel: LibraryViewModel,
settingsViewModel: SettingsViewModel
settingsViewModel: SettingsViewModel,
labelsViewModel: LabelsViewModel
) {
val hasAuthToken: Boolean by loginViewModel.hasAuthTokenLiveData.observeAsState(false)
val systemUiController = rememberSystemUiController()
@ -55,7 +57,8 @@ fun RootView(
loginViewModel = loginViewModel,
searchViewModel = searchViewModel,
libraryViewModel = libraryViewModel,
settingsViewModel = settingsViewModel
settingsViewModel = settingsViewModel,
labelsViewModel = labelsViewModel,
)
} else {
WelcomeScreen(viewModel = loginViewModel)
@ -75,7 +78,8 @@ fun PrimaryNavigator(
loginViewModel: LoginViewModel,
libraryViewModel: LibraryViewModel,
searchViewModel: SearchViewModel,
settingsViewModel: SettingsViewModel
settingsViewModel: SettingsViewModel,
labelsViewModel: LabelsViewModel,
) {
val navController = rememberNavController()
@ -83,7 +87,8 @@ fun PrimaryNavigator(
composable(Routes.Library.route) {
LibraryView(
libraryViewModel = libraryViewModel,
navController = navController
navController = navController,
labelsViewModel = labelsViewModel,
)
}

File diff suppressed because one or more lines are too long