Fix crash when creating labels during SetLabels operation
This commit is contained in:
@ -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
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
Reference in New Issue
Block a user