Merge remote-tracking branch 'upstream/main' into android/debug-package-name

This commit is contained in:
Gannon
2024-01-12 21:27:14 -05:00
53 changed files with 1194 additions and 1133 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}
}
}

View File

@ -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),

View File

@ -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)) },

View File

@ -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.

View File

@ -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>

View File

@ -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)
}

View 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>

View File

@ -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 -->

View File

@ -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 = "";

View File

@ -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

View File

@ -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"] }
}

View File

@ -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") },

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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") {

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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.";

View 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

View File

@ -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()

View File

@ -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

View File

@ -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'],

View 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);

View 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;

View 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);

View 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;

View 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);

View File

@ -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;

View 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);

View 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;

View 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);

View 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;

View 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;

View 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;

View File

@ -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
}
}
}

View File

@ -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"
}
}
}

View File

@ -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)
}
}

View File

@ -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')
}
}
)

View File

@ -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,

View File

@ -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"

955
yarn.lock

File diff suppressed because it is too large Load Diff