Merge remote-tracking branch 'upstream/main' into android/debug-package-name
This commit is contained in:
@ -19,8 +19,8 @@ android {
|
||||
applicationId "app.omnivore.omnivore"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 178
|
||||
versionName "0.0.178"
|
||||
versionCode 186
|
||||
versionName "0.0.186"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -114,14 +114,15 @@ suspend fun DataService.mergeWebHighlights(jsonString: String) {
|
||||
highlightPositionAnchorIndex = mergeHighlightInput.highlightPositionAnchorIndex.getOrNull() ?: 0
|
||||
)
|
||||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
||||
|
||||
saveHighlightChange(db.highlightChangesDao(), mergeHighlightInput.articleId, highlight)
|
||||
|
||||
Log.d("sync", "overlapHighlightIdList: " + mergeHighlightInput.overlapHighlightIdList)
|
||||
for (highlightID in mergeHighlightInput.overlapHighlightIdList) {
|
||||
deleteHighlight(mergeHighlightInput.articleId, highlightID)
|
||||
}
|
||||
val highlightChange = saveHighlightChange(
|
||||
db.highlightChangesDao(),
|
||||
mergeHighlightInput.articleId,
|
||||
highlight,
|
||||
html = mergeHighlightInput.html.getOrNull(),
|
||||
overlappingIDs = mergeHighlightInput.overlapHighlightIdList
|
||||
)
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = mergeHighlightInput.id,
|
||||
@ -131,11 +132,8 @@ suspend fun DataService.mergeWebHighlights(jsonString: String) {
|
||||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput)
|
||||
if (isUpdatedOnServer) {
|
||||
highlight.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
Log.d("sync", "Setting up highlight merge")
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.PrimaryKey
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
@ -9,6 +12,7 @@ import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.HighlightChange
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.highlightChangeToHighlight
|
||||
import app.omnivore.omnivore.persistence.entities.saveHighlightChange
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.log
|
||||
@ -112,8 +116,6 @@ private suspend fun DataService.syncHighlightChange(highlightChange: HighlightCh
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
Log.d("sync", "creating highlight update change: ${highlightChange}")
|
||||
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isUpdatedOnServer = networker.updateHighlight(
|
||||
@ -123,7 +125,6 @@ private suspend fun DataService.syncHighlightChange(highlightChange: HighlightCh
|
||||
sharedAt = Optional.absent()
|
||||
)
|
||||
)
|
||||
Log.d("sync", "sycn.updateHighlight result: ${isUpdatedOnServer}")
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
@ -134,21 +135,21 @@ private suspend fun DataService.syncHighlightChange(highlightChange: HighlightCh
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
Log.d("sync", "creating highlight create change: ${highlightChange}")
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val createResult = networker.createHighlight(
|
||||
CreateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
articleId = highlightChange.savedItemId,
|
||||
id = highlight.highlightId,
|
||||
patch = Optional.presentIfNotNull(highlight.patch),
|
||||
quote = Optional.presentIfNotNull(highlight.quote),
|
||||
shortId = highlight.shortId
|
||||
)
|
||||
val input = CreateHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
type = Optional.presentIfNotNull(HighlightType.safeValueOf(highlight.type)),
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
patch = Optional.presentIfNotNull(highlight.patch),
|
||||
quote = Optional.presentIfNotNull(highlight.quote),
|
||||
)
|
||||
Log.d("sync", "Creating highlight from input: ${input}")
|
||||
val createResult = networker.createHighlight(
|
||||
input
|
||||
)
|
||||
Log.d("sync", "sycn.createResult: " + createResult)
|
||||
|
||||
if (createResult.newHighlight != null || createResult.alreadyExists) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
return true
|
||||
@ -157,6 +158,58 @@ private suspend fun DataService.syncHighlightChange(highlightChange: HighlightCh
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_MERGE.rawValue -> {
|
||||
Log.d("sync", "NEEDS MERGE: ${highlightChange}")
|
||||
|
||||
val mergeHighlightInput = MergeHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
color = Optional.presentIfNotNull(highlight.color),
|
||||
highlightPositionAnchorIndex = Optional.presentIfNotNull(highlight.highlightPositionAnchorIndex),
|
||||
highlightPositionPercent = Optional.presentIfNotNull(highlight.highlightPositionPercent),
|
||||
html = Optional.presentIfNotNull(highlightChange.html),
|
||||
overlapHighlightIdList = highlightChange.overlappingIDs ?: emptyList(),
|
||||
patch = highlight.patch ?: "",
|
||||
prefix = Optional.presentIfNotNull(highlight.prefix),
|
||||
quote = highlight.quote ?: "",
|
||||
suffix = Optional.presentIfNotNull(highlight.suffix)
|
||||
)
|
||||
|
||||
val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput)
|
||||
if (!isUpdatedOnServer) {
|
||||
Log.d("sync", "FAILED TO MERGE HIGHLIGHT")
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
||||
return false
|
||||
}
|
||||
|
||||
for (highlightID in mergeHighlightInput.overlapHighlightIdList) {
|
||||
Log.d("sync", "DELETING MERGED HIGHLIGHT: ${highlightID}")
|
||||
val deleteChange = HighlightChange(
|
||||
highlightId = highlightID,
|
||||
savedItemId = highlightChange.savedItemId,
|
||||
type = "",
|
||||
shortId = "",
|
||||
annotation = null,
|
||||
createdAt = null,
|
||||
patch = null,
|
||||
prefix = null,
|
||||
quote = null,
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue,
|
||||
html = null,
|
||||
suffix = null,
|
||||
updatedAt = null,
|
||||
color = null,
|
||||
highlightPositionPercent = null,
|
||||
highlightPositionAnchorIndex = null,
|
||||
overlappingIDs = null
|
||||
)
|
||||
performHighlightChange(deleteChange)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,5 +5,6 @@ enum class ServerSyncStatus(val rawValue: Int) {
|
||||
IS_SYNCING(1),
|
||||
NEEDS_DELETION(2),
|
||||
NEEDS_CREATION(3),
|
||||
NEEDS_UPDATE(4)
|
||||
NEEDS_UPDATE(4),
|
||||
NEEDS_MERGE(5)
|
||||
}
|
||||
|
||||
@ -110,7 +110,6 @@ suspend fun Networker.updateWebHighlight(jsonString: String): Boolean {
|
||||
suspend fun Networker.updateHighlight(input: UpdateHighlightInput): Boolean {
|
||||
return try {
|
||||
val result = authenticatedApolloClient().mutation(UpdateHighlightMutation(input)).execute()
|
||||
Log.d("Network", "update highlight result: $result")
|
||||
result.data?.updateHighlight?.onUpdateHighlightSuccess?.highlight != null
|
||||
} catch (e: java.lang.Exception) {
|
||||
false
|
||||
@ -144,12 +143,8 @@ data class CreateHighlightResult(
|
||||
)
|
||||
|
||||
suspend fun Networker.createHighlight(input: CreateHighlightInput): CreateHighlightResult {
|
||||
Log.d("sync", "creating highlight with input: ${input}")
|
||||
|
||||
try {
|
||||
val result = authenticatedApolloClient().mutation(CreateHighlightMutation(input)).execute()
|
||||
Log.d("sync", "result: ${result.data}")
|
||||
|
||||
val createdHighlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight
|
||||
|
||||
if (createdHighlight != null) {
|
||||
|
||||
@ -14,7 +14,7 @@ import app.omnivore.omnivore.persistence.entities.*
|
||||
SavedItemAndSavedItemLabelCrossRef::class,
|
||||
SavedItemAndHighlightCrossRef::class
|
||||
],
|
||||
version = 20
|
||||
version = 24
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun viewerDao(): ViewerDao
|
||||
|
||||
@ -7,10 +7,16 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Entity
|
||||
@TypeConverters(StringListTypeConverter::class)
|
||||
data class HighlightChange(
|
||||
@PrimaryKey val highlightId: String,
|
||||
val savedItemId: String,
|
||||
@ -24,16 +30,45 @@ data class HighlightChange(
|
||||
var prefix: String?,
|
||||
var quote: String?,
|
||||
var serverSyncStatus: Int = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
val html: String?,
|
||||
var shortId: String,
|
||||
val suffix: String?,
|
||||
val updatedAt: String?,
|
||||
val color: String?,
|
||||
val highlightPositionPercent: Double?,
|
||||
val highlightPositionAnchorIndex: Int?
|
||||
val highlightPositionAnchorIndex: Int?,
|
||||
val overlappingIDs: List<String>?
|
||||
)
|
||||
|
||||
fun saveHighlightChange(dao: HighlightChangesDao, savedItemId: String, highlight: Highlight): HighlightChange {
|
||||
Log.d("sync", "saving highlight change: " + savedItemId + ", " + highlight)
|
||||
class StringListTypeConverter {
|
||||
@TypeConverter
|
||||
fun listToString(data: List<String>?): String? {
|
||||
data?.let {
|
||||
return Gson().toJson(data)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToList(jsonString: String?): List<String>? {
|
||||
return if (jsonString.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val itemType = object : TypeToken<List<String>>() {}.type
|
||||
return Gson().fromJson<List<String>>(jsonString, itemType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun saveHighlightChange(
|
||||
dao: HighlightChangesDao,
|
||||
savedItemId: String,
|
||||
highlight: Highlight,
|
||||
html: String? = null,
|
||||
overlappingIDs: List<String>? = null): HighlightChange {
|
||||
|
||||
Log.d("sync", "saving highlight change: " + highlight.serverSyncStatus + ", " + highlight.type)
|
||||
val change = HighlightChange(
|
||||
savedItemId = savedItemId,
|
||||
highlightId = highlight.highlightId,
|
||||
@ -43,6 +78,7 @@ fun saveHighlightChange(dao: HighlightChangesDao, savedItemId: String, highlight
|
||||
prefix = highlight.prefix,
|
||||
suffix = highlight.suffix,
|
||||
patch = highlight.patch,
|
||||
html = html,
|
||||
annotation = highlight.annotation,
|
||||
createdAt = highlight.createdAt,
|
||||
updatedAt = highlight.updatedAt,
|
||||
@ -50,7 +86,8 @@ fun saveHighlightChange(dao: HighlightChangesDao, savedItemId: String, highlight
|
||||
color =highlight.color,
|
||||
highlightPositionPercent = highlight.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = highlight.highlightPositionAnchorIndex,
|
||||
serverSyncStatus = highlight.serverSyncStatus
|
||||
serverSyncStatus = highlight.serverSyncStatus,
|
||||
overlappingIDs = overlappingIDs
|
||||
)
|
||||
dao.insertAll(listOf(change))
|
||||
return change
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
|
||||
|
||||
object AuthUtils {
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun Modifier.autofill(
|
||||
autofillTypes: List<AutofillType>,
|
||||
onFill: ((String) -> Unit),
|
||||
) = composed {
|
||||
val autofill = LocalAutofill.current
|
||||
val autofillNode = AutofillNode(onFill = onFill, autofillTypes = autofillTypes)
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
this
|
||||
.onGloballyPositioned {
|
||||
autofillNode.boundingBox = it.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,9 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@ -25,6 +27,7 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.auth.AuthUtils.autofill
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
@ -86,6 +89,7 @@ fun EmailLoginView(viewModel: LoginViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginFields(
|
||||
email: String,
|
||||
@ -105,6 +109,12 @@ fun LoginFields(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.EmailAddress,
|
||||
),
|
||||
onFill = { onEmailChange(it) }
|
||||
),
|
||||
value = email,
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_email)) },
|
||||
@ -112,11 +122,17 @@ fun LoginFields(
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Email,
|
||||
),
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.Password,
|
||||
),
|
||||
onFill = { onPasswordChange(it) }
|
||||
),
|
||||
value = password,
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_password)) },
|
||||
@ -129,21 +145,22 @@ fun LoginFields(
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
Button(onClick = {
|
||||
if (email.isNotBlank() && password.isNotBlank()) {
|
||||
onLoginClick()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.email_login_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D),
|
||||
containerColor = Color(0xffffd234)
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
if (email.isNotBlank() && password.isNotBlank()) {
|
||||
onLoginClick()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.email_login_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D),
|
||||
containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_login_action_login),
|
||||
|
||||
@ -14,7 +14,9 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@ -27,6 +29,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.auth.AuthUtils.autofill
|
||||
|
||||
@Composable
|
||||
fun EmailSignUpView(viewModel: LoginViewModel) {
|
||||
@ -140,6 +143,7 @@ fun EmailSignUpForm(viewModel: LoginViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun EmailSignUpFields(
|
||||
email: String,
|
||||
@ -165,6 +169,12 @@ fun EmailSignUpFields(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.EmailAddress,
|
||||
),
|
||||
onFill = { onEmailChange(it) }
|
||||
),
|
||||
value = email,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_email)) },
|
||||
@ -174,6 +184,12 @@ fun EmailSignUpFields(
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.Password,
|
||||
),
|
||||
onFill = { onPasswordChange(it) }
|
||||
),
|
||||
value = password,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_password)) },
|
||||
|
||||
@ -310,7 +310,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
||||
items = cardsData,
|
||||
key = { item -> item.savedItem.savedItemId }
|
||||
) { cardDataWithLabels ->
|
||||
val swipeThreshold = 0.40f
|
||||
val swipeThreshold = 0.45f
|
||||
|
||||
val currentThresholdFraction = remember { mutableStateOf(0f) }
|
||||
val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem)
|
||||
@ -320,7 +320,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
||||
currentThresholdFraction.value < swipeThreshold ||
|
||||
currentThresholdFraction.value > 1.0f
|
||||
) {
|
||||
false
|
||||
return@rememberDismissState false
|
||||
}
|
||||
|
||||
if (it == DismissValue.DismissedToEnd) { // Archiving/UnArchiving.
|
||||
|
||||
@ -61,6 +61,8 @@ data class WebReaderContent(
|
||||
|
||||
Log.d("theme", "current theme is: ${preferences.themeKey}")
|
||||
|
||||
Log.d("sync", "HIGHLIGHTS JSON: ${articleContent.highlightsJSONString()}")
|
||||
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
@ -324,6 +324,7 @@ class WebReaderViewModel @Inject constructor(
|
||||
// }
|
||||
|
||||
fun handleIncomingWebMessage(actionID: String, jsonString: String) {
|
||||
Log.d("sync", "incoming change: ${actionID}: ${jsonString}")
|
||||
when (actionID) {
|
||||
"createHighlight" -> {
|
||||
viewModelScope.launch {
|
||||
@ -331,13 +332,11 @@ class WebReaderViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
"deleteHighlight" -> {
|
||||
Log.d("Loggo", "receive delete highlight action: $jsonString")
|
||||
viewModelScope.launch {
|
||||
dataService.deleteHighlightFromJSON(jsonString)
|
||||
}
|
||||
}
|
||||
"updateHighlight" -> {
|
||||
Log.d("Loggo", "receive update highlight action: $jsonString")
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebHighlight(jsonString)
|
||||
}
|
||||
|
||||
233
android/Omnivore/app/src/main/res/values-de/strings.xml
Normal file
233
android/Omnivore/app/src/main/res/values-de/strings.xml
Normal file
@ -0,0 +1,233 @@
|
||||
<resources>
|
||||
<string name="app_name">Omnivore</string>
|
||||
<string name="welcome_title">Verpasse nie wieder eine großartige Lektüre</string>
|
||||
<string name="learn_more">Mehr erfahren</string>
|
||||
<string name="welcome_subtitle">Speichere Artikel und lies sie später in unserem ablenkungsfreien Reader.</string>
|
||||
<string name="highlight_menu_action">Markieren</string>
|
||||
<string name="copy_menu_action">Kopieren</string>
|
||||
<string name="annotate_menu_action">Anmerken</string>
|
||||
<string name="pdf_remove_highlight">Entfernen</string>
|
||||
<string name="pdf_highlight_menu_action">Markieren</string>
|
||||
<string name="pdf_highlight_copy">Kopieren</string>
|
||||
<string name="highlight_note">Notiz</string>
|
||||
<string name="copyTextSelection">Kopieren</string>
|
||||
<string name="pdf_highlight_menu_note">Notiz</string>
|
||||
|
||||
<!-- Apple Auth -->
|
||||
<string name="apple_auth_text">Mit Apple fortfahren</string>
|
||||
<string name="apple_auth_loading">Anmeldung...</string>
|
||||
|
||||
<!-- Create User Profile -->
|
||||
<string name="create_user_profile_title">Erstelle dein Profil</string>
|
||||
<string name="create_user_profile_loading">Laden...</string>
|
||||
<string name="create_user_profile_action_cancel">Anmeldung abbrechen</string>
|
||||
<string name="create_user_profile_action_submit">Absenden</string>
|
||||
<string name="create_user_profile_field_placeholder_name">Name</string>
|
||||
<string name="create_user_profile_field_label_name">Name</string>
|
||||
<string name="create_user_profile_field_placeholder_username">Benutzername</string>
|
||||
<string name="create_user_profile_field_label_username">Benutzername</string>
|
||||
<string name="create_user_profile_error_msg">Bitte gib einen gültigen Namen und Benutzernamen ein.</string>
|
||||
|
||||
<!-- Email Login -->
|
||||
<string name="email_login_loading">Laden...</string>
|
||||
<string name="email_login_action_back">Zurück zum Social Login</string>
|
||||
<string name="email_login_action_no_account">Du hast noch kein Konto?</string>
|
||||
<string name="email_login_action_forgot_password">Passwort vergessen?</string>
|
||||
<string name="email_login_action_login">Anmelden</string>
|
||||
<string name="email_login_field_placeholder_email">benutzer@email.com</string>
|
||||
<string name="email_login_field_label_email">E-Mail</string>
|
||||
<string name="email_login_field_placeholder_password">Passwort</string>
|
||||
<string name="email_login_field_label_password">Passwort</string>
|
||||
<string name="email_login_error_msg">Bitte gib eine E-Mail-Adresse und ein Passwort ein.</string>
|
||||
|
||||
<!-- Email Sign Up -->
|
||||
<string name="email_signup_verification_message">Wir haben eine Verifizierungs-E-Mail an %1$s gesendet. Bitte bestätige deine E-Mail und tippe dann auf den unten stehenden Knopf.</string>
|
||||
<string name="email_signup_check_status">Status prüfen</string>
|
||||
<string name="email_signup_action_use_different_email">Eine andere E-Mail verwenden?</string>
|
||||
<string name="email_signup_loading">Laden...</string>
|
||||
<string name="email_signup_action_back">Zurück zu Social Login</string>
|
||||
<string name="email_signup_action_already_have_account">Du hast bereits ein Konto?</string>
|
||||
<string name="email_signup_action_sign_up">Registrieren</string>
|
||||
<string name="email_signup_field_placeholder_email">benutzer@email.com</string>
|
||||
<string name="email_signup_field_label_email">E-Mail</string>
|
||||
<string name="email_signup_field_placeholder_password">Passwort</string>
|
||||
<string name="email_signup_field_label_password">Passwort</string>
|
||||
<string name="email_signup_field_placeholder_name">Name</string>
|
||||
<string name="email_signup_field_label_name">Name</string>
|
||||
<string name="email_signup_field_placeholder_username">Name</string>
|
||||
<string name="email_signup_field_label_username">Name</string>
|
||||
<string name="email_signup_error_msg">Bitte fülle alle Felder aus.</string>
|
||||
|
||||
<!-- Google Auth -->
|
||||
<string name="google_auth_text">Mit Google fortfahren</string>
|
||||
<string name="google_auth_loading">Anmeldung...</string>
|
||||
|
||||
<!-- LoginViewModel -->
|
||||
<string name="login_view_model_self_hosting_settings_updated">Einstellungen für Self-Hosting aktualisiert.</string>
|
||||
<string name="login_view_model_self_hosting_settings_reset">Einstellungen für Self-Hosting zurückgesetzt.</string>
|
||||
<string name="login_view_model_username_validation_length_error_msg">Benutzername muss zwischen 4 und 15 Zeichen lang sein.</string>
|
||||
<string name="login_view_model_username_validation_alphanumeric_error_msg">Benutzername darf nur Buchstaben und Zahlen enthalten.</string>
|
||||
<string name="login_view_model_username_not_available_error_msg">Dieser Benutzername ist nicht verfügbar.</string>
|
||||
<string name="login_view_model_connection_error_msg">Entschuldigung, wir haben Probleme, eine Verbindung zum Server herzustellen.</string>
|
||||
<string name="login_view_model_something_went_wrong_error_msg">Etwas ist schiefgelaufen. Bitte überprüfe deine E-Mail und dein Passwort und versuche es erneut.</string>
|
||||
<string name="login_view_model_something_went_wrong_two_error_msg">Etwas ist schiefgelaufen. Bitte überprüfe deine Anmeldeinformationen und versuche es erneut.</string>
|
||||
<string name="login_view_model_google_auth_error_msg">Authentifizierung mit Google fehlgeschlagen.</string>
|
||||
<string name="login_view_model_missing_auth_token_error_msg">Kein Authentifizierungstoken gefunden.</string>
|
||||
|
||||
<!-- SelfHostedView -->
|
||||
<string name="self_hosted_view_loading">Laden...</string>
|
||||
<string name="self_hosted_view_action_reset">Zurücksetzen</string>
|
||||
<string name="self_hosted_view_action_back">Zurück</string>
|
||||
<string name="self_hosted_view_action_save">Speichern</string>
|
||||
<string name="self_hosted_view_action_learn_more">Mehr über Self-Hosting von Omnivore erfahren</string>
|
||||
<string name="self_hosted_view_field_api_url_label">API-Server</string>
|
||||
<string name="self_hosted_view_field_web_url_label">Webserver</string>
|
||||
<string name="self_hosted_view_error_msg">Bitte gib die Adressen des API-Servers und des Webservers ein.</string>
|
||||
|
||||
<!-- WelcomeScreen -->
|
||||
<string name="welcome_screen_action_dismiss">Schließen</string>
|
||||
<string name="welcome_screen_action_continue_with_email">Mit E-Mail fortfahren</string>
|
||||
<string name="welcome_screen_action_self_hosting_options">Self-Hosting Optionen</string>
|
||||
|
||||
<!-- LabelCreationDialog -->
|
||||
<string name="label_creation_title">Neues Label erstellen</string>
|
||||
<string name="label_creation_content">Weise einen Namen und eine Farbe zu.</string>
|
||||
<string name="label_creation_action_create">Erstellen</string>
|
||||
<string name="label_creation_action_cancel">Abbrechen</string>
|
||||
<string name="label_creation_label_placeholder">Label-Name</string>
|
||||
|
||||
<!-- LabelSelectionSheet -->
|
||||
<string name="label_selection_sheet_title">Nach Label filtern</string>
|
||||
<string name="label_selection_sheet_title_alt">Labels setzen</string>
|
||||
<string name="label_selection_sheet_action_cancel">Abbrechen</string>
|
||||
<string name="label_selection_sheet_action_search">Suchen</string>
|
||||
<string name="label_selection_sheet_action_save">Speichern</string>
|
||||
<string name="label_selection_sheet_text_create">Erstelle ein neues Label mit dem Namen \"%1$s\"</string>
|
||||
<string name="label_selection_sheet_label_too_long_error_msg">Der angegebene Name ist zu lang (muss %1$d Zeichen oder weniger sein)</string>
|
||||
|
||||
<!-- LibraryFilterBar -->
|
||||
<string name="library_filter_bar_label_labels">Labels</string>
|
||||
|
||||
<!-- LibraryNavigationBar -->
|
||||
<string name="library_nav_bar_title">Bibliothek</string>
|
||||
<string name="library_nav_bar_title_alt"></string>
|
||||
<string name="library_nav_bar_field_placeholder_search">Suchen</string>
|
||||
|
||||
<!-- LibraryViewModel -->
|
||||
<string name="library_view_model_snackbar_success">Labels aktualisiert</string>
|
||||
<string name="library_view_model_snackbar_error">Labels konnten nicht gesetzt werden</string>
|
||||
|
||||
<!-- NotebookView -->
|
||||
<string name="notebook_view_title">Notizbuch</string>
|
||||
<string name="notebook_view_action_copy">Kopieren</string>
|
||||
<string name="notebook_view_snackbar_msg">Notizbuch kopiert</string>
|
||||
|
||||
<!-- EditNoteModal -->
|
||||
<string name="edit_note_modal_title">Notiz</string>
|
||||
<string name="edit_note_modal_action_save">Speichern</string>
|
||||
<string name="edit_note_modal_action_cancel">Abbrechen</string>
|
||||
|
||||
<!-- ArticleNotes -->
|
||||
<string name="article_notes_title">Artikelnotizen</string>
|
||||
<string name="article_notes_action_add_notes">Notizen hinzufügen...</string>
|
||||
|
||||
<!-- HighlightsList -->
|
||||
<string name="highlights_list_title">Hervorhebungen</string>
|
||||
<string name="highlights_list_action_copy">Kopieren</string>
|
||||
<string name="highlights_list_snackbar_msg">Hervorhebung kopiert</string>
|
||||
<string name="highlights_list_action_add_note">Notiz hinzufügen...</string>
|
||||
<string name="highlights_list_error_msg_no_highlights">Du hast dieser Seite keine Hervorhebungen hinzugefügt.</string>
|
||||
|
||||
<!-- ReaderPreferencesView -->
|
||||
<string name="reader_preferences_view_font_size">Schriftgröße:</string>
|
||||
<string name="reader_preferences_view_margin">Rand</string>
|
||||
<string name="reader_preferences_view_line_spacing">Zeilenabstand</string>
|
||||
<string name="reader_preferences_view_theme">Thema:</string>
|
||||
<string name="reader_preferences_view_auto">Automatisch</string>
|
||||
<string name="reader_preferences_view_high_constrast_text">Hoher Textkontrast</string>
|
||||
<string name="reader_preferences_view_justify_text">Text ausrichten</string>
|
||||
|
||||
<!-- WebReaderLoadingContainer -->
|
||||
<string name="web_reader_loading_container_error_msg">Wir konnten deinen Inhalt nicht abrufen.</string>
|
||||
<string name="web_reader_loading_container_bottom_sheet_reader_preferences">Lese-Einstellungen</string>
|
||||
<string name="web_reader_loading_container_bottom_sheet_notebook">Notizbuch</string>
|
||||
<string name="web_reader_loading_container_bottom_sheet_edit_info">Info bearbeiten</string>
|
||||
<string name="web_reader_loading_container_bottom_sheet_e">Notizbuch</string>
|
||||
<string name="web_reader_loading_container_bottom_sheet_open_link">Link öffnen</string>
|
||||
|
||||
<!-- OpenLinkView -->
|
||||
<string name="open_link_view_action_open_in_browser">Im Browser öffnen</string>
|
||||
<string name="open_link_view_action_save_to_omnivore">In Omnivore speichern</string>
|
||||
<string name="open_link_view_action_copy_link">Link kopieren</string>
|
||||
<string name="open_link_view_action_cancel">Abbrechen</string>
|
||||
|
||||
<!-- WebReaderViewModel -->
|
||||
<string name="web_reader_view_model_save_link_success">Link gespeichert</string>
|
||||
<string name="web_reader_view_model_save_link_error">Fehler beim Speichern des Links</string>
|
||||
<string name="web_reader_view_model_copy_link_success">Link kopiert</string>
|
||||
|
||||
<!-- SaveContent -->
|
||||
<string name="save_content_msg">Speichern</string>
|
||||
<string name="save_content_action_read_now">Jetzt lesen</string>
|
||||
<string name="save_content_action_read_later">Später lesen</string>
|
||||
<string name="save_content_action_dismiss">Schließen</string>
|
||||
|
||||
<!-- SaveViewModel -->
|
||||
<string name="save_view_model_msg">Speichern in Omnivore...</string>
|
||||
<string name="save_view_model_error_not_logged_in">Du bist nicht angemeldet. Bitte melde dich an, bevor du speicherst.</string>
|
||||
<string name="save_view_model_page_saved_success">Seite gespeichert</string>
|
||||
<string name="save_view_model_page_saved_error">Fehler beim Speichern deiner Seite</string>
|
||||
|
||||
<!-- SavedItemContextMenu -->
|
||||
<string name="saved_item_context_menu_action_edit_info">Info bearbeiten</string>
|
||||
<string name="saved_item_context_menu_action_edit_labels">Labels bearbeiten</string>
|
||||
<string name="saved_item_context_menu_action_archive">Archivieren</string>
|
||||
<string name="saved_item_context_menu_action_unarchive">Aus dem Archiv wiederherstellen</string>
|
||||
<string name="saved_item_context_menu_action_share_original">Original teilen</string>
|
||||
<string name="saved_item_context_menu_action_remove_item">Element entfernen</string>
|
||||
|
||||
<!-- LogoutDialog -->
|
||||
<string name="logout_dialog_title">Abmelden</string>
|
||||
<string name="logout_dialog_confirm_msg">Bist du sicher, dass du dich abmelden möchtest?</string>
|
||||
<string name="logout_dialog_action_confirm">Bestätigen</string>
|
||||
<string name="logout_dialog_action_cancel">Abbrechen</string>
|
||||
|
||||
<!-- ManageAccount -->
|
||||
<string name="manage_account_title">Konto verwalten</string>
|
||||
<string name="manage_account_action_reset_data_cache">Cache zurücksetzen</string>
|
||||
|
||||
<!-- PolicyWebView -->
|
||||
<string name="policy_webview_title">Einstellungen</string>
|
||||
|
||||
<!-- SettingsView -->
|
||||
<string name="settings_view_title">Einstellungen</string>
|
||||
<string name="settings_view_setting_row_documentation">Dokumentation</string>
|
||||
<string name="settings_view_setting_row_feedback">Feedback</string>
|
||||
<string name="settings_view_setting_row_privacy_policy">Datenschutzerklärung</string>
|
||||
<string name="settings_view_setting_row_terms_and_conditions">Nutzungsbedingungen</string>
|
||||
<string name="settings_view_setting_row_manage_account">Konto verwalten</string>
|
||||
<string name="settings_view_setting_row_logout">Abmelden</string>
|
||||
|
||||
<!-- AddLinkSheet -->
|
||||
<string name="add_link_sheet_title">Link hinzufügen</string>
|
||||
<string name="add_link_sheet_text_field_placeholder">Link hinzufügen</string>
|
||||
<string name="add_link_sheet_action_add_link">Hinzufügen</string>
|
||||
<string name="add_link_sheet_action_cancel">Abbrechen</string>
|
||||
<string name="add_link_sheet_action_paste_from_clipboard">Aus Zwischenablage holen</string>
|
||||
<string name="add_link_sheet_invalid_url_error">Ungültiger Link</string>
|
||||
<string name="add_link_sheet_save_url_error">Fehler beim Speichern des Links!</string>
|
||||
<string name="add_link_sheet_save_url_success">Link erfolgreich gespeichert!</string>
|
||||
|
||||
<!-- EditInfoViewModel -->
|
||||
<string name="edit_info_view_model_error_not_logged_in">Du bist nicht angemeldet. Bitte melde dich an, bevor du speicherst.</string>
|
||||
|
||||
<!-- EditInfoSheet -->
|
||||
<string name="edit_info_sheet_title">Info bearbeiten</string>
|
||||
<string name="edit_info_sheet_text_field_label_title">Titel</string>
|
||||
<string name="edit_info_sheet_text_field_label_author">Autor</string>
|
||||
<string name="edit_info_sheet_text_field_label_description">Beschreibung</string>
|
||||
<string name="edit_info_sheet_action_save">Speichern</string>
|
||||
<string name="edit_info_sheet_action_cancel">Abbrechen</string>
|
||||
<string name="edit_info_sheet_error">Fehler beim Bearbeiten des Artikels!</string>
|
||||
<string name="edit_info_sheet_success">Artikelinformationen erfolgreich aktualisiert!</string>
|
||||
</resources>
|
||||
@ -54,8 +54,8 @@
|
||||
<string name="email_signup_field_label_password">Password</string>
|
||||
<string name="email_signup_field_placeholder_name">Name</string>
|
||||
<string name="email_signup_field_label_name">Name</string>
|
||||
<string name="email_signup_field_placeholder_username">Name</string>
|
||||
<string name="email_signup_field_label_username">Name</string>
|
||||
<string name="email_signup_field_placeholder_username">Username</string>
|
||||
<string name="email_signup_field_label_username">Username</string>
|
||||
<string name="email_signup_error_msg">Please complete all fields.</string>
|
||||
|
||||
<!-- Google Auth -->
|
||||
|
||||
@ -1388,7 +1388,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.42.0;
|
||||
MARKETING_VERSION = 1.43.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
||||
@ -1423,7 +1423,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.42.0;
|
||||
MARKETING_VERSION = 1.43.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@ -1478,7 +1478,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.42.0;
|
||||
MARKETING_VERSION = 1.43.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
||||
PRODUCT_NAME = Omnivore;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -1819,7 +1819,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.42.0;
|
||||
MARKETING_VERSION = 1.43.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
|
||||
PRODUCT_NAME = Omnivore;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
#if os(iOS)
|
||||
import AppIntents
|
||||
import Services
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
public struct OmnivoreAppShorcuts: AppShortcutsProvider {
|
||||
@AppShortcutsBuilder public static var appShortcuts: [AppShortcut] {
|
||||
AppShortcut(intent: SaveToOmnivoreIntent(), phrases: ["Save URL to \(.applicationName)"])
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @available(iOS 16.0, *)
|
||||
// struct ExportAllTransactionsIntent: AppIntent {
|
||||
// static var title: LocalizedStringResource = "Export all transactions"
|
||||
//
|
||||
// static var description =
|
||||
// IntentDescription("Exports your transaction history as CSV data.")
|
||||
// }
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct SaveToOmnivoreIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Save to Omnivore"
|
||||
static var description: LocalizedStringResource = "Save a URL to your Omnivore library"
|
||||
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("Save \(\.$link) to your Omnivore library.")
|
||||
}
|
||||
|
||||
@Parameter(title: "link")
|
||||
var link: URL
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue {
|
||||
do {
|
||||
let services = Services()
|
||||
let requestId = UUID().uuidString.lowercased()
|
||||
_ = try await services.dataService.saveURL(id: requestId, url: link.absoluteString)
|
||||
|
||||
return .result(dialog: "Link saved to Omnivore")
|
||||
} catch {
|
||||
print("error saving URL: ", error)
|
||||
}
|
||||
return .result(dialog: "Error saving link")
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -44,6 +44,7 @@ import Utils
|
||||
|
||||
@State private var errorMessage: String?
|
||||
@State private var showNotebookView = false
|
||||
@State private var showLabelsModal = false
|
||||
@State private var hasPerformedHighlightMutations = false
|
||||
@State private var errorAlertMessage: String?
|
||||
@State private var showErrorAlertMessage = false
|
||||
@ -131,6 +132,12 @@ import Utils
|
||||
style: .plain,
|
||||
target: coordinator,
|
||||
action: #selector(PDFViewCoordinator.toggleNotebookView)
|
||||
),
|
||||
UIBarButtonItem(
|
||||
image: UIImage(named: "label", in: Bundle(url: ViewsPackage.bundleURL), with: nil),
|
||||
style: .plain,
|
||||
target: coordinator,
|
||||
action: #selector(PDFViewCoordinator.toggleLabelsView)
|
||||
)
|
||||
]
|
||||
|
||||
@ -228,14 +235,14 @@ import Utils
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
.fullScreenCover(isPresented: $readerView, content: {
|
||||
.sheet(isPresented: $readerView, content: {
|
||||
PDFReaderViewController(document: document)
|
||||
})
|
||||
.accentColor(Color(red: 255 / 255.0, green: 234 / 255.0, blue: 159 / 255.0))
|
||||
.sheet(item: $shareLink) {
|
||||
ShareSheet(activityItems: [$0.url])
|
||||
}
|
||||
.fullScreenCover(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
|
||||
.sheet(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
|
||||
NotebookView(
|
||||
viewModel: NotebookViewModel(item: viewModel.pdfItem.item),
|
||||
hasHighlightMutations: $hasPerformedHighlightMutations,
|
||||
@ -244,6 +251,17 @@ import Utils
|
||||
}
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showLabelsModal) {
|
||||
ApplyLabelsView(mode: .item(viewModel.pdfItem.item), onSave: { _ in
|
||||
showLabelsModal = false
|
||||
})
|
||||
}.task {
|
||||
viewModel.updateItemReadProgress(
|
||||
dataService: dataService,
|
||||
percent: viewModel.pdfItem.item.readingProgress,
|
||||
anchorIndex: Int(viewModel.pdfItem.item.readingProgressAnchor)
|
||||
)
|
||||
}
|
||||
} else if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
} else {
|
||||
@ -483,6 +501,12 @@ import Utils
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func toggleLabelsView() {
|
||||
if let viewer = self.viewer {
|
||||
viewer.showLabelsModal = !viewer.showLabelsModal
|
||||
}
|
||||
}
|
||||
|
||||
func shortHighlightIds(_ annotations: [HighlightAnnotation]) -> [String] {
|
||||
annotations.compactMap { ($0.customData?["omnivoreHighlight"] as? [String: String])?["shortId"] }
|
||||
}
|
||||
|
||||
@ -295,7 +295,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
LibraryAddLinkView()
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
|
||||
.sheet(isPresented: $showExpandedAudioPlayer) {
|
||||
ExpandedAudioPlayer(
|
||||
delete: {
|
||||
showExpandedAudioPlayer = false
|
||||
@ -330,7 +330,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
viewModel.selectedItem = linkedItem
|
||||
viewModel.linkIsActive = true
|
||||
}
|
||||
.fullScreenCover(isPresented: $searchPresented) {
|
||||
.sheet(isPresented: $searchPresented) {
|
||||
LibrarySearchView(homeFeedViewModel: self.viewModel)
|
||||
}
|
||||
.task {
|
||||
@ -573,17 +573,17 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
HStack {
|
||||
Menu(content: {
|
||||
Button(action: {
|
||||
viewModel.fetcher.updateFeatureFilter(context: dataService.viewContext, filter: .continueReading)
|
||||
viewModel.updateFeatureFilter(context: dataService.viewContext, filter: .continueReading)
|
||||
}, label: {
|
||||
Text("Continue Reading")
|
||||
})
|
||||
Button(action: {
|
||||
viewModel.fetcher.updateFeatureFilter(context: dataService.viewContext, filter: .pinned)
|
||||
viewModel.updateFeatureFilter(context: dataService.viewContext, filter: .pinned)
|
||||
}, label: {
|
||||
Text("Pinned")
|
||||
})
|
||||
Button(action: {
|
||||
viewModel.fetcher.updateFeatureFilter(context: dataService.viewContext, filter: .newsletters)
|
||||
viewModel.updateFeatureFilter(context: dataService.viewContext, filter: .newsletters)
|
||||
}, label: {
|
||||
Text("Newsletters")
|
||||
})
|
||||
@ -597,7 +597,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: "line.3.horizontal.decrease")
|
||||
.font(Font.system(size: 13, weight: .regular))
|
||||
Text((FeaturedItemFilter(rawValue: viewModel.fetcher.featureFilter) ?? .continueReading).title)
|
||||
Text((FeaturedItemFilter(rawValue: viewModel.featureFilter) ?? .continueReading).title)
|
||||
.font(Font.system(size: 13, weight: .medium))
|
||||
}
|
||||
.tint(Color(hex: "#007AFF"))
|
||||
@ -753,6 +753,9 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
await viewModel.loadMore(dataService: dataService)
|
||||
}
|
||||
}
|
||||
|
||||
// reload this in case it was changed in settings
|
||||
viewModel.hideFeatureSection = UserDefaults.standard.bool(forKey: UserDefaultKey.hideFeatureSection.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -884,7 +887,9 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
case .delete:
|
||||
return AnyView(Button(
|
||||
action: {
|
||||
viewModel.removeLibraryItem(dataService: dataService, objectID: item.objectID)
|
||||
withAnimation(.linear(duration: 0.4)) {
|
||||
viewModel.removeLibraryItem(dataService: dataService, objectID: item.objectID)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
@ -893,7 +898,9 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
case .moveToInbox:
|
||||
return AnyView(Button(
|
||||
action: {
|
||||
viewModel.moveToFolder(dataService: dataService, item: item, folder: "inbox")
|
||||
withAnimation(.linear(duration: 0.4)) {
|
||||
viewModel.moveToFolder(dataService: dataService, item: item, folder: "inbox")
|
||||
}
|
||||
},
|
||||
label: {
|
||||
Label(title: { Text("Move to Library") },
|
||||
|
||||
@ -49,6 +49,8 @@ enum LoadingBarStyle {
|
||||
@AppStorage(UserDefaultKey.stopUsingFollowingPrimer.rawValue) var stopUsingFollowingPrimer = false
|
||||
@AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false
|
||||
|
||||
@AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter = FeaturedItemFilter.continueReading.rawValue
|
||||
|
||||
@Published var appliedFilter: InternalFilter? {
|
||||
didSet {
|
||||
if let filterName = appliedFilter?.name.lowercased() {
|
||||
@ -363,4 +365,11 @@ enum LoadingBarStyle {
|
||||
snackbar("Error modifying emails")
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeatureFilter(context: NSManagedObjectContext, filter: FeaturedItemFilter?) {
|
||||
if let filter = filter {
|
||||
featureFilter = filter.rawValue
|
||||
fetcher.updateFeatureFilter(context: context, filter: filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,27 +48,32 @@ public struct LibrarySplitView: View {
|
||||
$0.preferredPrimaryColumnWidth = 230
|
||||
$0.displayModeButtonVisibility = .always
|
||||
}
|
||||
// .onOpenURL { url in
|
||||
// inboxViewModel.linkRequest = nil
|
||||
// if let deepLink = DeepLink.make(from: url) {
|
||||
// switch deepLink {
|
||||
// case let .search(query):
|
||||
// inboxViewModel.searchTerm = query
|
||||
// case let .savedSearch(named):
|
||||
// if let filter = inboxViewModel.findFilter(dataService, named: named) {
|
||||
// inboxViewModel.appliedFilter = filter
|
||||
// }
|
||||
// case let .webAppLinkRequest(requestID):
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
// withoutAnimation {
|
||||
// inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
|
||||
// inboxViewModel.presentWebContainer = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// // selectedTab = "inbox"
|
||||
// }
|
||||
.onOpenURL { url in
|
||||
viewModel.linkRequest = nil
|
||||
|
||||
withoutAnimation {
|
||||
NotificationCenter.default.post(Notification(name: Notification.Name("PopToRoot")))
|
||||
}
|
||||
|
||||
if let deepLink = DeepLink.make(from: url) {
|
||||
switch deepLink {
|
||||
case let .search(query):
|
||||
viewModel.searchTerm = query
|
||||
case let .savedSearch(named):
|
||||
if let filter = viewModel.findFilter(dataService, named: named) {
|
||||
viewModel.appliedFilter = filter
|
||||
}
|
||||
case let .webAppLinkRequest(requestID):
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
withoutAnimation {
|
||||
viewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
|
||||
viewModel.presentWebContainer = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||
Task {
|
||||
await syncManager.syncUpdates(dataService: dataService)
|
||||
|
||||
@ -108,7 +108,7 @@ struct LibraryTabView: View {
|
||||
.padding(0)
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
|
||||
.sheet(isPresented: $showExpandedAudioPlayer) {
|
||||
ExpandedAudioPlayer(
|
||||
delete: {
|
||||
showExpandedAudioPlayer = false
|
||||
@ -135,6 +135,11 @@ struct LibraryTabView: View {
|
||||
}
|
||||
.onOpenURL { url in
|
||||
inboxViewModel.linkRequest = nil
|
||||
|
||||
withoutAnimation {
|
||||
NotificationCenter.default.post(Notification(name: Notification.Name("PopToRoot")))
|
||||
}
|
||||
|
||||
if let deepLink = DeepLink.make(from: url) {
|
||||
switch deepLink {
|
||||
case let .search(query):
|
||||
@ -144,6 +149,7 @@ struct LibraryTabView: View {
|
||||
inboxViewModel.appliedFilter = filter
|
||||
}
|
||||
case let .webAppLinkRequest(requestID):
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
withoutAnimation {
|
||||
inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
|
||||
|
||||
@ -162,6 +162,15 @@ struct ProfileView: View {
|
||||
)
|
||||
#endif
|
||||
|
||||
Button(
|
||||
action: {
|
||||
if let url = URL(string: "https://discord.gg/h2z5rppzz9") {
|
||||
openURL(url)
|
||||
}
|
||||
},
|
||||
label: { Text("Join community on Discord") }
|
||||
)
|
||||
|
||||
Button(
|
||||
action: {
|
||||
if let url = URL(string: "https://omnivore.app/privacy") {
|
||||
|
||||
@ -210,7 +210,6 @@ struct WebReaderContainerView: View {
|
||||
},
|
||||
label: { Label("Reset Read Location", systemImage: "arrow.counterclockwise.circle") }
|
||||
)
|
||||
audioMenuItem()
|
||||
|
||||
if viewModel.hasOriginalUrl(item) {
|
||||
Button(
|
||||
@ -340,28 +339,6 @@ struct WebReaderContainerView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundColor(ThemeManager.currentTheme.toolbarColor)
|
||||
.background(ThemeManager.currentBgColor)
|
||||
.sheet(isPresented: $showLabelsModal) {
|
||||
ApplyLabelsView(mode: .item(item), onSave: { labels in
|
||||
showLabelsModal = false
|
||||
item.labels = NSSet(array: labels)
|
||||
readerSettingsChangedTransactionID = UUID()
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $showTitleEdit) {
|
||||
LinkedItemMetadataEditView(item: item, onSave: { title, _ in
|
||||
item.title = title
|
||||
// We dont need to update description because its never rendered in this view
|
||||
readerSettingsChangedTransactionID = UUID()
|
||||
})
|
||||
}
|
||||
#if os(iOS)
|
||||
.sheet(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
|
||||
NotebookView(
|
||||
viewModel: NotebookViewModel(item: item),
|
||||
hasHighlightMutations: $hasPerformedHighlightMutations
|
||||
)
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
#endif
|
||||
@ -421,9 +398,12 @@ struct WebReaderContainerView: View {
|
||||
.statusBar(hidden: prefersHideStatusBarInReader)
|
||||
#endif
|
||||
.onAppear {
|
||||
if item.isUnread {
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0, force: false)
|
||||
}
|
||||
dataService.updateLinkReadingProgress(
|
||||
itemID: item.unwrappedID,
|
||||
readingProgress: max(item.readingProgress, 0.1),
|
||||
anchorIndex: Int(item.readingProgressAnchor),
|
||||
force: false
|
||||
)
|
||||
Task {
|
||||
await audioController.preload(itemIDs: [item.unwrappedID])
|
||||
}
|
||||
@ -450,11 +430,11 @@ struct WebReaderContainerView: View {
|
||||
}, label: { Text(LocalText.readerSave) })
|
||||
}
|
||||
#if os(iOS)
|
||||
.fullScreenCover(item: $safariWebLink) {
|
||||
.sheet(item: $safariWebLink) {
|
||||
SafariView(url: $0.url)
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
}
|
||||
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
|
||||
.sheet(isPresented: $showExpandedAudioPlayer) {
|
||||
ExpandedAudioPlayer(delete: { _ in
|
||||
showExpandedAudioPlayer = false
|
||||
audioController.stop()
|
||||
@ -519,6 +499,28 @@ struct WebReaderContainerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showLabelsModal) {
|
||||
ApplyLabelsView(mode: .item(item), onSave: { labels in
|
||||
showLabelsModal = false
|
||||
item.labels = NSSet(array: labels)
|
||||
readerSettingsChangedTransactionID = UUID()
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $showTitleEdit) {
|
||||
LinkedItemMetadataEditView(item: item, onSave: { title, _ in
|
||||
item.title = title
|
||||
// We dont need to update description because its never rendered in this view
|
||||
readerSettingsChangedTransactionID = UUID()
|
||||
})
|
||||
}
|
||||
#if os(iOS)
|
||||
.sheet(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
|
||||
NotebookView(
|
||||
viewModel: NotebookViewModel(item: item),
|
||||
hasHighlightMutations: $hasPerformedHighlightMutations
|
||||
)
|
||||
}
|
||||
#endif
|
||||
} else if let errorMessage = viewModel.errorMessage {
|
||||
VStack {
|
||||
if viewModel.allowRetry, viewModel.hasOriginalUrl(item) {
|
||||
@ -620,6 +622,9 @@ struct WebReaderContainerView: View {
|
||||
// WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
|
||||
WebViewManager.shared().loadHTMLString(WebReaderContent.emptyContent(isDark: Color.isDarkMode), baseURL: nil)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PopToRoot"))) { _ in
|
||||
pop()
|
||||
}
|
||||
.popup(isPresented: $viewModel.showSnackbar) {
|
||||
if let operation = viewModel.snackbarOperation {
|
||||
Snackbar(isShowing: $viewModel.showSnackbar, operation: operation)
|
||||
|
||||
@ -286,7 +286,8 @@ public extension LibraryItem {
|
||||
newAuthor: String? = nil,
|
||||
listenPositionIndex: Int? = nil,
|
||||
listenPositionOffset: Double? = nil,
|
||||
listenPositionTime: Double? = nil
|
||||
listenPositionTime: Double? = nil,
|
||||
readAt: Date? = nil
|
||||
) {
|
||||
context.perform {
|
||||
if let newReadingProgress = newReadingProgress {
|
||||
@ -325,6 +326,10 @@ public extension LibraryItem {
|
||||
self.listenPositionTime = listenPositionTime
|
||||
}
|
||||
|
||||
if let readAt = readAt {
|
||||
self.readAt = readAt
|
||||
}
|
||||
|
||||
guard context.hasChanges else { return }
|
||||
self.updatedAt = Date()
|
||||
|
||||
|
||||
@ -9,17 +9,12 @@ extension DataService {
|
||||
guard let self = self else { return }
|
||||
guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return }
|
||||
|
||||
if let force = force, !force {
|
||||
if readingProgress != 0, readingProgress < linkedItem.readingProgress {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
print("updating reading progress: ", readingProgress, anchorIndex)
|
||||
linkedItem.update(
|
||||
inContext: self.backgroundContext,
|
||||
newReadingProgress: readingProgress,
|
||||
newAnchorIndex: anchorIndex
|
||||
newAnchorIndex: anchorIndex,
|
||||
readAt: Date()
|
||||
)
|
||||
|
||||
// Send update to server
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,208 @@
|
||||
// Unit test Entry -- Do not remove this or add entries before this one.
|
||||
// This allows us to check for syntax errors in this file with a unit test
|
||||
"unitTestLeadingEntry" = "Nur zu Testzwecken.";
|
||||
|
||||
// share extension
|
||||
"saveArticleSavedState" = "In Omnivore gespeichert";
|
||||
"saveArticleProcessingState" = "Wird in Omnivore gespeichert";
|
||||
"extensionAppUnauthorized" = "Bitte melde dich in der App bei Omnivore an, bevor du deinen ersten Link speicherst.";
|
||||
"saveToOmnivore" = "In Omnivore speichern";
|
||||
|
||||
// audio player
|
||||
"audioPlayerReplay" = "Wiederholen";
|
||||
|
||||
// Highlights List Card
|
||||
"highlightCardHighlightByOther" = "Markierung von ";
|
||||
"highlightCardNoHighlightsOnPage" = "Du hast keine Markierungen auf dieser Seite hinzugefügt.";
|
||||
|
||||
// Labels View
|
||||
"labelsViewAssignNameColor" = "Weise einen Namen und eine Farbe zu.";
|
||||
"createLabelMessage" = "Erstelle ein neues Label";
|
||||
"labelsPurposeDescription" = "Nutze Labels, um Sammlungen von Links zu erstellen.";
|
||||
"labelNamePlaceholder" = "Label Name";
|
||||
|
||||
// Manage Account View
|
||||
"manageAccountDelete" = "Konto löschen";
|
||||
"manageAccountResetCache" = "Cache zurücksetzen";
|
||||
"manageAccountConfirmDeleteMessage" = "Bist du sicher, dass du dein Konto löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.";
|
||||
|
||||
// Newsletter Emails View
|
||||
"newsletterEmailsExisting" = "Vorhandene E-Mails (Tippen zum Kopieren)";
|
||||
"createNewEmailMessage" = "Erstelle eine neue E-Mail-Adresse";
|
||||
"newslettersDescription" = "Füge PDFs zu deiner Bibliothek hinzu oder abonniere Newsletter mit einer Omnivore-E-Mail-Adresse.";
|
||||
"noCurrentSubscriptionsMessage" = "Du hast aktuell keine Abonnements.";
|
||||
|
||||
// Profile View
|
||||
"profileConfirmLogoutMessage" = "Bist du sicher, dass du dich abmelden möchtest?";
|
||||
|
||||
// Devices View
|
||||
"devicesTokensTitle" = "Registrierte Geräte-Tokens (wischen zum Entfernen)";
|
||||
"devicesCreated" = "Erstellt: ";
|
||||
|
||||
// Push Notification Settings
|
||||
"notificationsEnabled" = "Benachrichtigungen aktiviert";
|
||||
"notificationsExplainer" = "Das Aktivieren von Push-Benachrichtigungen gibt Omnivore die Erlaubnis, Benachrichtigungen zu senden,\ndu entscheidest jedoch, welche Benachrichtigungen gesendet werden.";
|
||||
"notificationsTriggerExplainer" = "Push-Benachrichtigungen werden durch deine \n[Kontoregeln](https://omnivore.app/settings/rules) ausgelöst, die du online bearbeiten kannst.";
|
||||
"notificationsEnable" = "Push-Benachrichtigungen aktivieren?";
|
||||
"notificationsGeneralExplainer" = "Erhalte Benachrichtigungen, wenn Newsletter-Links in deinem Posteingang eintreffen. Oder erhalte Erinnerungen, die du über unsere Erweiterung festgelegt hast.";
|
||||
"notificationsOptionDeny" = "Nein, danke";
|
||||
"notificationsOptionEnable" = "Ja, bitte";
|
||||
|
||||
// Community Modal
|
||||
"communityHeadline" = "Hilf mit, die Omnivore-Community aufzubauen";
|
||||
"communityAppstoreReview" = "Bewerte uns im AppStore";
|
||||
"communityTweet" = "Tweete über Omnivore";
|
||||
"communityFollowTwitter" = "Folge uns auf Twitter";
|
||||
"communityJoinDiscord" = "Tritt unserem Discord bei";
|
||||
"communityStarGithub" = "Gib uns ein Stern auf GitHub";
|
||||
|
||||
// Clubs View
|
||||
"clubsLearnTitle" = "Erfahre mehr über Clubs";
|
||||
"clubsName" = "Club Name";
|
||||
"clubsCreate" = "Erstelle einen neuen Club";
|
||||
"clubsYours" = "Deine Clubs";
|
||||
"clubsNotAMemberMessage" = "Du bist kein Mitglied eines Clubs.\nErstelle einen neuen Club und sende den Einladungslink an deine Freunde, um loszulegen.\n\nWährend der Beta bist du darauf beschränkt, drei Clubs zu erstellen, und jeder Club\nkann maximal zwölf Benutzer haben.";
|
||||
"clubsErrorCopying" = "Fehler beim Kopieren des Einladungs-Links";
|
||||
"clubsAdminDenyViewing" = "Der Admin dieses Clubs erlaubt es nicht, alle Mitglieder einzusehen.";
|
||||
"clubsNoMembers" = "Dieser Club hat keine Mitglieder. Füge Benutzer zu deinem Club hinzu, indem du\nihnen den Einladungslink sendest.";
|
||||
"clubsLeave" = "Club verlassen";
|
||||
"clubsLeaveConfirm" = "Bist du sicher, dass du diesen Club verlassen möchtest? Es werden keine Daten gelöscht, aber du wirst keine Empfehlungen mehr von diesem Club erhalten.";
|
||||
"clubsNoneJoined" = "Du bist keinem Club beigetreten, in dem du posten kannst.\nTritt einem Club bei oder erstelle deinen eigenen, um Artikel zu empfehlen.";
|
||||
|
||||
// Subscriptions
|
||||
"subscriptionsErrorRetrieving" = "Entschuldigung, wir konnten deine Abonnements nicht abrufen.";
|
||||
"subscriptionsNone" = "Du hast aktuell keine Abonnements.";
|
||||
//"subscriptions.error.retrieving" = "Zuletzt erhalten: (updatedDate.formatted())"; // unused for now
|
||||
|
||||
// Text to Speech
|
||||
"texttospeechLanguageDefault" = "Standardsprache";
|
||||
"texttospeechSettingsAudio" = "Audio Einstellungen";
|
||||
"texttospeechSettingsEnablePrefetch" = "Audio Vorladen aktivieren";
|
||||
"texttospeechBetaSignupInProcess" = "Anmeldung zur Beta läuft";
|
||||
"texttospeechBetaRealisticVoiceLimit" = "Du nimmst an der Beta für ultra-realistische Stimmen teil. Während der Beta kannst du 10.000 Wörter Audio pro Tag anhören.";
|
||||
"texttospeechBetaRequestReceived" = "Deine Anfrage, an der Demo für ultra-realistische Stimmen teilzunehmen, wurde erhalten. Du wirst per E-Mail informiert, wenn ein Platz verfügbar ist.";
|
||||
"texttospeechBetaWaitlist" = "Ultra-realistische Stimmen sind derzeit in einer begrenzten Beta und nur für Englisch verfügbar. Das Aktivieren der Funktion wird dich zur Beta-Warteliste hinzufügen.";
|
||||
|
||||
// Sign in/up
|
||||
"registrationNoAccount" = "Du hast noch kein Konto?";
|
||||
"registrationForgotPassword" = "Passwort vergessen?";
|
||||
"registrationStatusCheck" = "Status überprüfen";
|
||||
"registrationUseDifferentEmail" = "Eine andere E-Mail verwenden?";
|
||||
"registrationFullName" = "Vollständiger Name";
|
||||
"registrationUsername" = "Benutzername";
|
||||
"registrationAlreadyHaveAccount" = "Du hast bereits ein Konto?";
|
||||
"registrationBio" = "Biografie (optional)";
|
||||
"registrationWelcome" = "Willkommen bei Omnivore!";
|
||||
"registrationUsernameAssignedPrefix" = "Dein Benutzername lautet:";
|
||||
"registrationChangeUsername" = "Benutzername ändern";
|
||||
"registrationEdit" = "Bearbeiten";
|
||||
"googleAuthButton" = "Mit Google fortfahren";
|
||||
"registrationViewSignUpHeadline" = "Registrieren";
|
||||
"loginErrorInvalidCreds" = "Die angegebenen Anmeldeinformationen sind ungültig.";
|
||||
|
||||
// Recommendation
|
||||
"recommendationToPrefix" = "An:";
|
||||
"recommendationAddNote" = "Eine Notiz hinzufügen (optional)";
|
||||
//"recommendationToPrefix" = "Füge deine (viewModel.highlightCount) Markierung(viewModel.highlightCount > 1 ? "en" : """; // unused for now
|
||||
"recommendationError" = "Fehler beim Empfehlen dieser Seite";
|
||||
|
||||
// Web Reader
|
||||
"readerCopyLink" = "Link kopieren";
|
||||
"readerSave" = "In Omnivore speichern";
|
||||
"readerError" = "Ein Fehler ist aufgetreten";
|
||||
|
||||
// Debug Menu
|
||||
"menuDebugTitle" = "Debuggin Menü";
|
||||
"menuDebugApiEnv" = "API Umgebung:";
|
||||
|
||||
// Navigation
|
||||
"navigationSelectLink" = "Wähle einen Link aus deiner Bibliothek";
|
||||
"navigationSelectSidebarToggle" = "Seitenleiste umschalten";
|
||||
|
||||
// Welcome View
|
||||
"welcomeTitle" = "Read-it-later für anspruchsvolle Leser.";
|
||||
"welcomeLearnMore" = "Mehr erfahren";
|
||||
"welcomeSignupAgreement" = "Mit deiner Anmeldung stimmst du den\n";
|
||||
"welcomeTitleTermsOfService" = "Nutzungsbedingungen";
|
||||
"welcomeTitleAndJoiner" = " und ";
|
||||
"welcomeTitleEmailContinue" = "Mit E-Mail fortfahren";
|
||||
|
||||
// Keyboard Commands
|
||||
"keyboardCommandDecreaseFont" = "Schriftgröße verkleinern";
|
||||
"keyboardCommandIncreaseFont" = "Schriftgröße vergrößern";
|
||||
"keyboardCommandDecreaseMargin" = "Rand verkleinern";
|
||||
"keyboardCommandIncreaseMargin" = "Rand vergrößern";
|
||||
"keyboardCommandDecreaseLineSpacing" = "Zeilenabstand verkleinern";
|
||||
"keyboardCommandIncreaseLineSpacing" = "Zeilenabstand vergrößern";
|
||||
|
||||
// Library
|
||||
//"library.by.author.suffix" = "von (author)" // unused
|
||||
//"Recommended by (byStr) in (inStr)" // unused
|
||||
|
||||
// Generic
|
||||
"genericSnooze" = "Schlummern";
|
||||
"genericClose" = "Schließen";
|
||||
"genericCreate" = "Erstellen";
|
||||
"genericConfirm" = "Bestätigen";
|
||||
"genericProfile" = "Profil";
|
||||
"genericNext" = "Weiter";
|
||||
"genericName" = "Name";
|
||||
"genericOk" = "Ok";
|
||||
"genericRetry" = "Erneut versuchen";
|
||||
"genericEmail" = "E-Mail";
|
||||
"genericPassword" = "Passwort";
|
||||
"genericSubmit" = "Absenden";
|
||||
"genericContinue" = "Fortfahren";
|
||||
"genericSend" = "Senden";
|
||||
"genericOptions" = "Optionen";
|
||||
"genericOpen" = "Öffnen";
|
||||
"genericChangeApply" = "Änderungen anwenden";
|
||||
"genericTitle" = "Titel";
|
||||
"genericAuthor" = "Autor";
|
||||
"genericDescription" = "Beschreibung";
|
||||
"genericSave" = "Speichern";
|
||||
"genericLoading" = "Lädt...";
|
||||
"genericFontFamily" = "Schriftart";
|
||||
"genericHighContrastText" = "Text in hohem Kontrast";
|
||||
"enableHighlightOnReleaseText" = "Automatisches Markieren aktivieren";
|
||||
"enableJustifyText" = "Text ausrichten";
|
||||
"genericFont" = "Schrift";
|
||||
"genericHighlight" = "Hervorheben";
|
||||
"labelsGeneric" = "Labels";
|
||||
"emailsGeneric" = "E-Mails";
|
||||
"subscriptionsGeneric" = "Abonnements";
|
||||
"textToSpeechGeneric" = "Text in Sprache";
|
||||
"privacyPolicyGeneric" = "Datenschutzrichtlinie";
|
||||
"termsAndConditionsGeneric" = "Geschäftsbedingungen";
|
||||
"feedbackGeneric" = "Feedback";
|
||||
"manageAccountGeneric" = "Konto verwalten";
|
||||
"logoutGeneric" = "Abmelden";
|
||||
"doneGeneric" = "Fertig";
|
||||
"cancelGeneric" = "Abbrechen";
|
||||
"exportGeneric" = "Exportieren";
|
||||
"inboxGeneric" = "Posteingang";
|
||||
"readLaterGeneric" = "Später lesen";
|
||||
"newslettersGeneric" = "Newsletter";
|
||||
"allGeneric" = "Alle";
|
||||
"archivedGeneric" = "Archiviert";
|
||||
"highlightedGeneric" = "Markiert";
|
||||
"filesGeneric" = "Dateien";
|
||||
"newestGeneric" = "Neueste";
|
||||
"oldestGeneric" = "Älteste";
|
||||
"longestGeneric" = "Längste";
|
||||
"shortestGeneric" = "Kürzeste";
|
||||
"recentlyReadGeneric" = "Kürzlich gelesen";
|
||||
"recentlyPublishedGeneric" = "Kürzlich veröffentlicht";
|
||||
"clubsGeneric" = "Clubs";
|
||||
"filterGeneric" = "Filter";
|
||||
"errorGeneric" = "Etwas ist schiefgelaufen, bitte versuche es erneut.";
|
||||
"pushNotificationsGeneric" = "Push-Benachrichtigungen";
|
||||
"dismissButton" = "Verwerfen";
|
||||
"errorNetwork" = "Wir haben Probleme, eine Verbindung zum Internet herzustellen.";
|
||||
"documentationGeneric" = "Dokumentation";
|
||||
|
||||
// TODO: search navigationTitle, toggle, section, button, Label, title: ", CreateProfileViewModel, TextField, .keyboardShortcut
|
||||
|
||||
// Unit test Entry -- Do not remove this or add entries after this one.
|
||||
// This allows us to check for syntax errors in this file with a unit test
|
||||
"unitTestTrailingEntry" = "Nur zu Testzwecken.";
|
||||
0
apple/Resources/de.lproj/LaunchScreen.strings
Normal file
0
apple/Resources/de.lproj/LaunchScreen.strings
Normal file
@ -1,6 +1,7 @@
|
||||
#if os(iOS)
|
||||
import App
|
||||
import AppIntents
|
||||
import CoreData
|
||||
import Firebase
|
||||
import FirebaseMessaging
|
||||
import Foundation
|
||||
@ -9,6 +10,84 @@
|
||||
import UIKit
|
||||
import Utils
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
func filterQuery(predicte: NSPredicate, sort: NSSortDescriptor, limit: Int = 10) async throws -> [LibraryItemEntity] {
|
||||
let context = await Services().dataService.viewContext
|
||||
let fetchRequest: NSFetchRequest<Models.LibraryItem> = LibraryItem.fetchRequest()
|
||||
fetchRequest.fetchLimit = limit
|
||||
fetchRequest.predicate = predicte
|
||||
fetchRequest.sortDescriptors = [sort]
|
||||
|
||||
return try context.performAndWait {
|
||||
do {
|
||||
return try context.fetch(fetchRequest).map { LibraryItemEntity(item: $0) }
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct LibraryItemEntity: AppEntity {
|
||||
static var defaultQuery = LibraryItemQuery()
|
||||
|
||||
let id: UUID
|
||||
|
||||
@Property(title: "Title")
|
||||
var title: String
|
||||
@Property(title: "Orignal URL")
|
||||
var originalURL: String?
|
||||
@Property(title: "Omnivore web URL")
|
||||
var omnivoreWebURL: String
|
||||
@Property(title: "Omnivore deeplink URL")
|
||||
var omnivoreShortcutURL: String
|
||||
@Property(title: "Author if set")
|
||||
var author: String?
|
||||
@Property(title: "Site name if set")
|
||||
var siteName: String?
|
||||
@Property(title: "Published date if set")
|
||||
var publishedAt: Date?
|
||||
@Property(title: "Time the item was saved")
|
||||
var savedAt: Date?
|
||||
|
||||
init(item: Models.LibraryItem) {
|
||||
self.id = UUID(uuidString: item.unwrappedID)!
|
||||
self.title = item.unwrappedTitle
|
||||
self.originalURL = item.pageURLString
|
||||
self.omnivoreWebURL = "https://omnivore.app/me/\(item.slug!)"
|
||||
self.omnivoreShortcutURL = "omnivore://read/\(item.unwrappedID)"
|
||||
self.author = item.author
|
||||
self.siteName = item.siteName
|
||||
self.publishedAt = item.publishDate
|
||||
self.savedAt = item.savedAt
|
||||
}
|
||||
|
||||
static var typeDisplayRepresentation = TypeDisplayRepresentation(
|
||||
stringLiteral: "Library Item"
|
||||
)
|
||||
|
||||
var displayRepresentation: DisplayRepresentation {
|
||||
DisplayRepresentation(title: "\(title)")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct LibraryItemQuery: EntityQuery {
|
||||
func entities(for itemIds: [UUID]) async throws -> [LibraryItemEntity] {
|
||||
let predicate = NSPredicate(format: "id IN %@", itemIds)
|
||||
let sort = FeaturedItemFilter.continueReading.sortDescriptor // sort by read recency
|
||||
return try await filterQuery(predicte: predicate, sort: sort)
|
||||
}
|
||||
|
||||
func suggestedEntities() async throws -> [LibraryItemEntity] {
|
||||
try await filterQuery(
|
||||
predicte: FeaturedItemFilter.continueReading.predicate,
|
||||
sort: FeaturedItemFilter.continueReading.sortDescriptor,
|
||||
limit: 10
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
public struct OmnivoreAppShorcuts: AppShortcutsProvider {
|
||||
@AppShortcutsBuilder public static var appShortcuts: [AppShortcut] {
|
||||
@ -16,15 +95,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @available(iOS 16.0, *)
|
||||
// struct ExportAllTransactionsIntent: AppIntent {
|
||||
// static var title: LocalizedStringResource = "Export all transactions"
|
||||
//
|
||||
// static var description =
|
||||
// IntentDescription("Exports your transaction history as CSV data.")
|
||||
// }
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct SaveToOmnivoreIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Save to Omnivore"
|
||||
@ -71,4 +141,77 @@
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.4, *)
|
||||
struct GetMostRecentLibraryItem: AppIntent {
|
||||
static let title: LocalizedStringResource = "Get most recently read library item"
|
||||
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<LibraryItemEntity?> {
|
||||
let result = try await filterQuery(
|
||||
predicte: LinkedItemFilter.all.predicate,
|
||||
sort: FeaturedItemFilter.continueReading.sortDescriptor,
|
||||
limit: 10
|
||||
)
|
||||
|
||||
if let result = result.first {
|
||||
return .result(value: result)
|
||||
}
|
||||
return .result(value: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.4, *)
|
||||
struct GetContinueReadingLibraryItems: AppIntent {
|
||||
static let title: LocalizedStringResource = "Get your continue reading library items"
|
||||
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<[LibraryItemEntity]> {
|
||||
let result = try await filterQuery(
|
||||
predicte: FeaturedItemFilter.continueReading.predicate,
|
||||
sort: FeaturedItemFilter.continueReading.sortDescriptor,
|
||||
limit: 10
|
||||
)
|
||||
|
||||
return .result(value: result)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.4, *)
|
||||
struct GetFollowingLibraryItems: AppIntent {
|
||||
static let title: LocalizedStringResource = "Get your following library items"
|
||||
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<[LibraryItemEntity]> {
|
||||
let savedAtSort = NSSortDescriptor(key: #keyPath(Models.LibraryItem.savedAt), ascending: false)
|
||||
let folderPredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(Models.LibraryItem.folder), "following"
|
||||
)
|
||||
|
||||
let result = try await filterQuery(
|
||||
predicte: folderPredicate,
|
||||
sort: savedAtSort,
|
||||
limit: 10
|
||||
)
|
||||
|
||||
return .result(value: result)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.4, *)
|
||||
struct GetSavedLibraryItems: AppIntent {
|
||||
static let title: LocalizedStringResource = "Get your saved library items"
|
||||
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<[LibraryItemEntity]> {
|
||||
let savedAtSort = NSSortDescriptor(key: #keyPath(Models.LibraryItem.savedAt), ascending: false)
|
||||
let folderPredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(Models.LibraryItem.folder), "inbox"
|
||||
)
|
||||
|
||||
let result = try await filterQuery(
|
||||
predicte: folderPredicate,
|
||||
sort: savedAtSort,
|
||||
limit: 10
|
||||
)
|
||||
|
||||
return .result(value: result)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@ -93,6 +93,7 @@ export enum SortOrder {
|
||||
export interface Sort {
|
||||
by: string
|
||||
order?: SortOrder
|
||||
nulls?: 'NULLS FIRST' | 'NULLS LAST'
|
||||
}
|
||||
|
||||
interface Select {
|
||||
@ -332,8 +333,10 @@ export const buildQuery = (
|
||||
|
||||
const order =
|
||||
sortOrder === 'asc' ? SortOrder.ASCENDING : SortOrder.DESCENDING
|
||||
const nulls =
|
||||
order === SortOrder.ASCENDING ? 'NULLS FIRST' : 'NULLS LAST'
|
||||
|
||||
orders.push({ by: `library_item.${column}`, order })
|
||||
orders.push({ by: `library_item.${column}`, order, nulls })
|
||||
return null
|
||||
}
|
||||
case 'has':
|
||||
@ -613,12 +616,13 @@ export const searchLibraryItems = async (
|
||||
orders.push({
|
||||
by: 'library_item.saved_at',
|
||||
order: SortOrder.DESCENDING,
|
||||
nulls: 'NULLS LAST',
|
||||
})
|
||||
}
|
||||
|
||||
// add order by
|
||||
orders.forEach((order) => {
|
||||
queryBuilder.addOrderBy(order.by, order.order, 'NULLS LAST')
|
||||
queryBuilder.addOrderBy(order.by, order.order, order.nulls)
|
||||
})
|
||||
|
||||
const libraryItems = await queryBuilder.skip(from).take(size).getMany()
|
||||
|
||||
@ -1722,6 +1722,7 @@ describe('Article API', () => {
|
||||
readableContent: '<p>test 1</p>',
|
||||
slug: 'test slug 1',
|
||||
originalUrl: `${url}/test1`,
|
||||
savedAt: new Date(1703880588),
|
||||
},
|
||||
{
|
||||
user,
|
||||
@ -1729,6 +1730,7 @@ describe('Article API', () => {
|
||||
readableContent: '<p>test 2</p>',
|
||||
slug: 'test slug 2',
|
||||
originalUrl: `${url}/test2`,
|
||||
savedAt: new Date(1704880589),
|
||||
},
|
||||
{
|
||||
user,
|
||||
@ -1736,6 +1738,7 @@ describe('Article API', () => {
|
||||
readableContent: '<p>test 3</p>',
|
||||
slug: 'test slug 3',
|
||||
originalUrl: `${url}/test3`,
|
||||
savedAt: new Date(1705880590),
|
||||
},
|
||||
],
|
||||
user.id
|
||||
@ -1777,6 +1780,7 @@ describe('Article API', () => {
|
||||
readableContent: '<p>test 1</p>',
|
||||
slug: 'test slug 1',
|
||||
originalUrl: `${url}/test1`,
|
||||
savedAt: new Date(1703880588),
|
||||
},
|
||||
{
|
||||
user,
|
||||
@ -1784,6 +1788,7 @@ describe('Article API', () => {
|
||||
readableContent: '<p>test 2</p>',
|
||||
slug: 'test slug 2',
|
||||
originalUrl: `${url}/test2`,
|
||||
savedAt: new Date(1704880589),
|
||||
},
|
||||
{
|
||||
user,
|
||||
@ -1791,6 +1796,7 @@ describe('Article API', () => {
|
||||
readableContent: '<p>test 3</p>',
|
||||
slug: 'test slug 3',
|
||||
originalUrl: `${url}/test3`,
|
||||
savedAt: new Date(1705880590),
|
||||
},
|
||||
],
|
||||
user.id
|
||||
|
||||
@ -14,7 +14,6 @@ const mutation = async (name, input) => {
|
||||
actionID: name,
|
||||
...input,
|
||||
})
|
||||
console.log('action result', name, result, result.result)
|
||||
return result.result
|
||||
} else {
|
||||
// Send android a message
|
||||
@ -33,6 +32,7 @@ const mutation = async (name, input) => {
|
||||
case 'mergeHighlight':
|
||||
return {
|
||||
id: input['id'],
|
||||
type: input['type'],
|
||||
shortID: input['shortId'],
|
||||
quote: input['quote'],
|
||||
patch: input['patch'],
|
||||
|
||||
6
packages/db/migrations/0153.do.library_item_user_id_saved_at_idx.sql
Executable file
6
packages/db/migrations/0153.do.library_item_user_id_saved_at_idx.sql
Executable file
@ -0,0 +1,6 @@
|
||||
-- Type: DO
|
||||
-- Name: library_item_user_id_saved_at_idx
|
||||
-- Description: Add library_item_user_id_saved_at_idx index on library_item table for user_id and saved_at
|
||||
|
||||
-- create index for sorting concurrently to avoid locking
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS library_item_user_id_saved_at_idx ON omnivore.library_item (user_id, saved_at DESC NULLS LAST);
|
||||
9
packages/db/migrations/0153.undo.library_item_user_id_saved_at_idx.sql
Executable file
9
packages/db/migrations/0153.undo.library_item_user_id_saved_at_idx.sql
Executable file
@ -0,0 +1,9 @@
|
||||
-- Type: UNDO
|
||||
-- Name: library_item_user_id_saved_at_idx
|
||||
-- Description: Add library_item_user_id_saved_at_idx index on library_item table for user_id and saved_at
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS omnivore.library_item_user_id_saved_at_idx;
|
||||
|
||||
COMMIT;
|
||||
6
packages/db/migrations/0154.do.library_item_user_id_updated_at_idx.sql
Executable file
6
packages/db/migrations/0154.do.library_item_user_id_updated_at_idx.sql
Executable file
@ -0,0 +1,6 @@
|
||||
-- Type: DO
|
||||
-- Name: library_item_user_id_updated_at_idx
|
||||
-- Description: Add library_item_user_id_saved_at_idx index on library_item table for user_id and updated_at
|
||||
|
||||
-- create index for sorting concurrently to avoid locking
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS library_item_user_id_updated_at_idx ON omnivore.library_item (user_id, updated_at DESC NULLS LAST);
|
||||
9
packages/db/migrations/0154.undo.library_item_user_id_updated_at_idx.sql
Executable file
9
packages/db/migrations/0154.undo.library_item_user_id_updated_at_idx.sql
Executable file
@ -0,0 +1,9 @@
|
||||
-- Type: UNDO
|
||||
-- Name: library_item_user_id_updated_at_idx
|
||||
-- Description: Add library_item_user_id_saved_at_idx index on library_item table for user_id and updated_at
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS library_item_user_id_updated_at_idx;
|
||||
|
||||
COMMIT;
|
||||
6
packages/db/migrations/0155.do.library_item_user_id_published_at_idx.sql
Executable file
6
packages/db/migrations/0155.do.library_item_user_id_published_at_idx.sql
Executable file
@ -0,0 +1,6 @@
|
||||
-- Type: DO
|
||||
-- Name: library_item_user_id_published_at_idx
|
||||
-- Description: Add library_item_user_id_published_at_idx index on library_item table for user_id and published_at
|
||||
|
||||
-- create index for sorting concurrently to avoid locking
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS library_item_user_id_published_at_idx ON omnivore.library_item (user_id, published_at DESC NULLS LAST);
|
||||
@ -0,0 +1,9 @@
|
||||
-- Type: UNDO
|
||||
-- Name: library_item_user_id_published_at_idx
|
||||
-- Description: Add library_item_user_id_published_at_idx index on library_item table for user_id and published_at
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS library_item_user_id_published_at_idx;
|
||||
|
||||
COMMIT;
|
||||
6
packages/db/migrations/0156.do.library_item_user_id_read_at_idx.sql
Executable file
6
packages/db/migrations/0156.do.library_item_user_id_read_at_idx.sql
Executable file
@ -0,0 +1,6 @@
|
||||
-- Type: DO
|
||||
-- Name: library_item_user_id_read_at_idx
|
||||
-- Description: Add library_item_user_id_read_at_idx index on library_item table for user_id and read_at
|
||||
|
||||
-- create index for sorting concurrently to avoid locking
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS library_item_user_id_read_at_idx ON omnivore.library_item (user_id, read_at DESC NULLS LAST);
|
||||
9
packages/db/migrations/0156.undo.library_item_user_id_read_at_idx.sql
Executable file
9
packages/db/migrations/0156.undo.library_item_user_id_read_at_idx.sql
Executable file
@ -0,0 +1,9 @@
|
||||
-- Type: UNDO
|
||||
-- Name: library_item_user_id_read_at_idx
|
||||
-- Description: Add library_item_user_id_read_at_idx index on library_item table for user_id and read_at
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS library_item_user_id_read_at_idx;
|
||||
|
||||
COMMIT;
|
||||
6
packages/db/migrations/0157.do.library_item_user_id_word_count_idx.sql
Executable file
6
packages/db/migrations/0157.do.library_item_user_id_word_count_idx.sql
Executable file
@ -0,0 +1,6 @@
|
||||
-- Type: DO
|
||||
-- Name: library_item_user_id_word_count_idx
|
||||
-- Description: Add library_item_user_id_word_count_idx index on library_item table for user_id and word_count
|
||||
|
||||
-- create index for sorting concurrently to avoid locking
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS library_item_user_id_word_count_idx ON omnivore.library_item (user_id, word_count DESC NULLS LAST);
|
||||
9
packages/db/migrations/0157.undo.library_item_user_id_word_count_idx.sql
Executable file
9
packages/db/migrations/0157.undo.library_item_user_id_word_count_idx.sql
Executable file
@ -0,0 +1,9 @@
|
||||
-- Type: UNDO
|
||||
-- Name: library_item_user_id_word_count_idx
|
||||
-- Description: Add library_item_user_id_word_count_idx index on library_item table for user_id and word_count
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS library_item_user_id_word_count_idx;
|
||||
|
||||
COMMIT;
|
||||
34
packages/db/migrations/0158.do.create_label_names_update_trigger.sql
Executable file
34
packages/db/migrations/0158.do.create_label_names_update_trigger.sql
Executable file
@ -0,0 +1,34 @@
|
||||
-- Type: DO
|
||||
-- Name: create_label_names_update_trigger
|
||||
-- Description: Create label_names_update trigger in library_item table
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_label_names()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE omnivore.library_item
|
||||
SET label_names = array_replace(label_names, OLD.name, NEW.name)
|
||||
WHERE user_id = OLD.user_id AND OLD.name = ANY(label_names);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- triggers when label name is updated
|
||||
CREATE TRIGGER label_names_update
|
||||
AFTER UPDATE ON omnivore.labels
|
||||
FOR EACH ROW
|
||||
WHEN (OLD.name <> NEW.name)
|
||||
EXECUTE FUNCTION update_label_names();
|
||||
|
||||
-- remove old trigger which is too slow
|
||||
DROP TRIGGER IF EXISTS entity_labels_update ON omnivore.labels;
|
||||
|
||||
DROP FUNCTION IF EXISTS omnivore.update_entity_labels();
|
||||
|
||||
DROP INDEX IF EXISTS omnivore.library_item_saved_at_idx;
|
||||
DROP INDEX IF EXISTS omnivore.library_item_updated_at_idx;
|
||||
DROP INDEX IF EXISTS omnivore.library_item_read_at_idx;;
|
||||
|
||||
COMMIT;
|
||||
34
packages/db/migrations/0158.undo.create_label_names_update_trigger.sql
Executable file
34
packages/db/migrations/0158.undo.create_label_names_update_trigger.sql
Executable file
@ -0,0 +1,34 @@
|
||||
-- Type: UNDO
|
||||
-- Name: create_label_names_update_trigger
|
||||
-- Description: Create label_names_update trigger in library_item table
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS library_item_saved_at_idx ON omnivore.library_item (saved_at);
|
||||
CREATE INDEX IF NOT EXISTS library_item_updated_at_idx ON omnivore.library_item (updated_at);
|
||||
CREATE INDEX IF NOT EXISTS library_item_read_at_idx ON omnivore.library_item (read_at);
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_entity_labels()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
-- update entity_labels table to trigger update on library_item table
|
||||
UPDATE omnivore.entity_labels
|
||||
SET label_id = NEW.id
|
||||
WHERE label_id = OLD.id;
|
||||
|
||||
return NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- triggers when label name is updated
|
||||
CREATE TRIGGER entity_labels_update
|
||||
AFTER UPDATE ON omnivore.labels
|
||||
FOR EACH ROW
|
||||
WHEN (OLD.name <> NEW.name)
|
||||
EXECUTE FUNCTION update_entity_labels();
|
||||
|
||||
DROP TRIGGER IF EXISTS label_names_update ON omnivore.labels;
|
||||
|
||||
DROP FUNCTION IF EXISTS omnivore.update_label_names();
|
||||
|
||||
COMMIT;
|
||||
@ -1,14 +1,20 @@
|
||||
{
|
||||
"extends": [
|
||||
"canonical",
|
||||
"canonical/node",
|
||||
"canonical/typescript"
|
||||
],
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
"project": "tsconfig.json"
|
||||
},
|
||||
"root": true,
|
||||
"rules": {
|
||||
"@typescript-eslint/no-parameter-properties": 0
|
||||
"semi": 0,
|
||||
"fp/no-class": 0,
|
||||
"prettier/prettier": 0,
|
||||
"@typescript-eslint/ban-types": 0,
|
||||
"@typescript-eslint/no-unsafe-call": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/no-inferrable-types": 0,
|
||||
"@typescript-eslint/no-unsafe-argument": 0,
|
||||
"@typescript-eslint/no-unsafe-member-access": 0,
|
||||
"@typescript-eslint/no-parameter-properties": 0,
|
||||
"@typescript-eslint/no-unsafe-assignment": 0,
|
||||
"@typescript-eslint/restrict-template-expressions": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,8 +28,6 @@
|
||||
"benny": "^3.7.1",
|
||||
"coveralls": "^3.1.1",
|
||||
"del-cli": "^4.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-canonical": "^28.0.0",
|
||||
"faker": "^5.5.3",
|
||||
"husky": "^7.0.4",
|
||||
"npm-watch": "^0.11.0",
|
||||
@ -67,4 +65,4 @@
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,14 @@
|
||||
/* eslint-disable fp/no-class */
|
||||
|
||||
import {
|
||||
ExtendableError,
|
||||
} from 'ts-error';
|
||||
import { ExtendableError } from 'ts-error'
|
||||
|
||||
export class LiqeError extends ExtendableError {}
|
||||
|
||||
export class SyntaxError extends LiqeError {
|
||||
public constructor (
|
||||
public constructor(
|
||||
public message: string,
|
||||
public offset: number,
|
||||
public line: number,
|
||||
public column: number,
|
||||
public column: number
|
||||
) {
|
||||
super(message);
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
@ -605,18 +605,17 @@ export const rssHandler = Sentry.GCPFunction.wrapHttpFunction(
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
// create redis client
|
||||
const redisClient = await createRedisClient(
|
||||
process.env.REDIS_URL,
|
||||
process.env.REDIS_CERT
|
||||
)
|
||||
|
||||
try {
|
||||
if (!isRssFeedRequest(req.body)) {
|
||||
console.error('Invalid request body', req.body)
|
||||
return res.status(400).send('INVALID_REQUEST_BODY')
|
||||
}
|
||||
|
||||
// create redis client
|
||||
const redisClient = await createRedisClient(
|
||||
process.env.REDIS_URL,
|
||||
process.env.REDIS_CERT
|
||||
)
|
||||
|
||||
const {
|
||||
feedUrl,
|
||||
subscriptionIds,
|
||||
@ -678,6 +677,9 @@ export const rssHandler = Sentry.GCPFunction.wrapHttpFunction(
|
||||
} catch (e) {
|
||||
console.error('Error while saving RSS feeds', e)
|
||||
res.status(500).send('INTERNAL_SERVER_ERROR')
|
||||
} finally {
|
||||
await redisClient.quit()
|
||||
console.log('Redis client disconnected')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -8,7 +8,10 @@ import {
|
||||
} from './highlightGenerator'
|
||||
import type { HighlightLocation } from './highlightGenerator'
|
||||
import { extendRangeToWordBoundaries } from './normalizeHighlightRange'
|
||||
import type { Highlight } from '../networking/fragments/highlightFragment'
|
||||
import type {
|
||||
Highlight,
|
||||
HighlightType,
|
||||
} from '../networking/fragments/highlightFragment'
|
||||
import { removeHighlights } from './deleteHighlight'
|
||||
import { ArticleMutations } from '../articleActions'
|
||||
import { NodeHtmlMarkdown } from 'node-html-markdown'
|
||||
@ -103,6 +106,7 @@ export async function createHighlight(
|
||||
id,
|
||||
shortId: nanoid(8),
|
||||
patch,
|
||||
type: 'HIGHLIGHT' as HighlightType,
|
||||
|
||||
color: input.color,
|
||||
prefix: highlightAttributes.prefix,
|
||||
|
||||
@ -2830,9 +2830,9 @@ fn.name@1.x.x:
|
||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||
|
||||
follow-redirects@^1.14.0:
|
||||
version "1.14.8"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
|
||||
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
|
||||
version "1.15.4"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
|
||||
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
|
||||
|
||||
formidable@^1.0.17:
|
||||
version "1.2.2"
|
||||
|
||||
Reference in New Issue
Block a user