mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge branch 'omnivore-app:main' into main
This commit is contained in:
commit
08e86fe729
84 changed files with 2256 additions and 805 deletions
|
|
@ -19,8 +19,8 @@ android {
|
|||
applicationId "app.omnivore.omnivore"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 158
|
||||
versionName "0.0.158"
|
||||
versionCode 180
|
||||
versionName "0.0.180"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,10 @@
|
|||
query UpdatesSince($after: String, $first: Int, $since: Date!) {
|
||||
updatesSince(after: $after, first: $first, since: $since) {
|
||||
query UpdatesSince(
|
||||
$folder: String
|
||||
$after: String
|
||||
$first: Int
|
||||
$since: Date!
|
||||
) {
|
||||
updatesSince(after: $after, first: $first, folder: $folder, since: $since) {
|
||||
... on UpdatesSinceSuccess {
|
||||
edges {
|
||||
cursor
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ type Article {
|
|||
contentReader: ContentReader!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
folder: String!
|
||||
hasContent: Boolean
|
||||
hash: String!
|
||||
highlights(input: ArticleHighlightsInput): [Highlight!]!
|
||||
|
|
@ -154,6 +155,7 @@ union ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequ
|
|||
|
||||
enum ArticleSavingRequestStatus {
|
||||
ARCHIVED
|
||||
CONTENT_NOT_FETCHED
|
||||
DELETED
|
||||
FAILED
|
||||
PROCESSING
|
||||
|
|
@ -203,6 +205,7 @@ enum BulkActionType {
|
|||
ARCHIVE
|
||||
DELETE
|
||||
MARK_AS_READ
|
||||
MOVE_TO_FOLDER
|
||||
}
|
||||
|
||||
enum ContentReader {
|
||||
|
|
@ -227,8 +230,12 @@ enum CreateArticleErrorCode {
|
|||
|
||||
input CreateArticleInput {
|
||||
articleSavingRequestId: ID
|
||||
folder: String
|
||||
labels: [CreateLabelInput!]
|
||||
preparedDocument: PreparedDocumentInput
|
||||
publishedAt: Date
|
||||
rssFeedUrl: String
|
||||
savedAt: Date
|
||||
skipParsing: Boolean
|
||||
source: String
|
||||
state: ArticleSavingRequestStatus
|
||||
|
|
@ -377,6 +384,12 @@ enum CreateNewsletterEmailErrorCode {
|
|||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input CreateNewsletterEmailInput {
|
||||
description: String
|
||||
folder: String
|
||||
name: String
|
||||
}
|
||||
|
||||
union CreateNewsletterEmailResult = CreateNewsletterEmailError | CreateNewsletterEmailSuccess
|
||||
|
||||
type CreateNewsletterEmailSuccess {
|
||||
|
|
@ -631,6 +644,20 @@ type DeviceTokensSuccess {
|
|||
deviceTokens: [DeviceToken!]!
|
||||
}
|
||||
|
||||
type EmptyTrashError {
|
||||
errorCodes: [EmptyTrashErrorCode!]!
|
||||
}
|
||||
|
||||
enum EmptyTrashErrorCode {
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union EmptyTrashResult = EmptyTrashError | EmptyTrashSuccess
|
||||
|
||||
type EmptyTrashSuccess {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
type Feature {
|
||||
createdAt: Date!
|
||||
expiresAt: Date
|
||||
|
|
@ -641,6 +668,19 @@ type Feature {
|
|||
updatedAt: Date
|
||||
}
|
||||
|
||||
type Feed {
|
||||
author: String
|
||||
createdAt: Date
|
||||
description: String
|
||||
id: ID
|
||||
image: String
|
||||
publishedAt: Date
|
||||
title: String!
|
||||
type: String
|
||||
updatedAt: Date
|
||||
url: String!
|
||||
}
|
||||
|
||||
type FeedArticle {
|
||||
annotationsCount: Int
|
||||
article: Article!
|
||||
|
|
@ -674,12 +714,56 @@ type FeedArticlesSuccess {
|
|||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type FeedEdge {
|
||||
cursor: String!
|
||||
node: Feed!
|
||||
}
|
||||
|
||||
type FeedsError {
|
||||
errorCodes: [FeedsErrorCode!]!
|
||||
}
|
||||
|
||||
enum FeedsErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input FeedsInput {
|
||||
after: String
|
||||
first: Int
|
||||
query: String
|
||||
sort: SortParams
|
||||
}
|
||||
|
||||
union FeedsResult = FeedsError | FeedsSuccess
|
||||
|
||||
type FeedsSuccess {
|
||||
edges: [FeedEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type FetchContentError {
|
||||
errorCodes: [FetchContentErrorCode!]!
|
||||
}
|
||||
|
||||
enum FetchContentErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union FetchContentResult = FetchContentError | FetchContentSuccess
|
||||
|
||||
type FetchContentSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Filter {
|
||||
category: String!
|
||||
category: String
|
||||
createdAt: Date!
|
||||
defaultFilter: Boolean
|
||||
description: String
|
||||
filter: String!
|
||||
folder: String
|
||||
id: ID!
|
||||
name: String!
|
||||
position: Int!
|
||||
|
|
@ -901,6 +985,8 @@ type IntegrationsSuccess {
|
|||
integrations: [Integration!]!
|
||||
}
|
||||
|
||||
scalar JSON
|
||||
|
||||
type JoinGroupError {
|
||||
errorCodes: [JoinGroupErrorCode!]!
|
||||
}
|
||||
|
|
@ -925,6 +1011,7 @@ type Label {
|
|||
internal: Boolean
|
||||
name: String!
|
||||
position: Int
|
||||
source: String
|
||||
}
|
||||
|
||||
type LabelsError {
|
||||
|
|
@ -1107,15 +1194,31 @@ type MoveLabelSuccess {
|
|||
label: Label!
|
||||
}
|
||||
|
||||
type MoveToFolderError {
|
||||
errorCodes: [MoveToFolderErrorCode!]!
|
||||
}
|
||||
|
||||
enum MoveToFolderErrorCode {
|
||||
ALREADY_EXISTS
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union MoveToFolderResult = MoveToFolderError | MoveToFolderSuccess
|
||||
|
||||
type MoveToFolderSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
addPopularRead(name: String!): AddPopularReadResult!
|
||||
bulkAction(action: BulkActionType!, async: Boolean, expectedCount: Int, labelIds: [ID!], query: String!): BulkActionResult!
|
||||
bulkAction(action: BulkActionType!, arguments: JSON, async: Boolean, expectedCount: Int, labelIds: [ID!], query: String!): BulkActionResult!
|
||||
createArticle(input: CreateArticleInput!): CreateArticleResult!
|
||||
createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult!
|
||||
createGroup(input: CreateGroupInput!): CreateGroupResult!
|
||||
createHighlight(input: CreateHighlightInput!): CreateHighlightResult!
|
||||
createLabel(input: CreateLabelInput!): CreateLabelResult!
|
||||
createNewsletterEmail: CreateNewsletterEmailResult!
|
||||
createNewsletterEmail(input: CreateNewsletterEmailInput): CreateNewsletterEmailResult!
|
||||
deleteAccount(userID: ID!): DeleteAccountResult!
|
||||
deleteFilter(id: ID!): DeleteFilterResult!
|
||||
deleteHighlight(highlightId: ID!): DeleteHighlightResult!
|
||||
|
|
@ -1124,6 +1227,8 @@ type Mutation {
|
|||
deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
|
||||
deleteRule(id: ID!): DeleteRuleResult!
|
||||
deleteWebhook(id: ID!): DeleteWebhookResult!
|
||||
emptyTrash: EmptyTrashResult!
|
||||
fetchContent(id: ID!): FetchContentResult!
|
||||
generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult!
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
googleSignup(input: GoogleSignupInput!): GoogleSignupResult!
|
||||
|
|
@ -1135,6 +1240,7 @@ type Mutation {
|
|||
mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult!
|
||||
moveFilter(input: MoveFilterInput!): MoveFilterResult!
|
||||
moveLabel(input: MoveLabelInput!): MoveLabelResult!
|
||||
moveToFolder(folder: String!, id: ID!): MoveToFolderResult!
|
||||
optInFeature(input: OptInFeatureInput!): OptInFeatureResult!
|
||||
recommend(input: RecommendInput!): RecommendResult!
|
||||
recommendHighlights(input: RecommendHighlightsInput!): RecommendHighlightsResult!
|
||||
|
|
@ -1161,6 +1267,7 @@ type Mutation {
|
|||
updateFilter(input: UpdateFilterInput!): UpdateFilterResult!
|
||||
updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult!
|
||||
updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
|
||||
updateNewsletterEmail(input: UpdateNewsletterEmailInput!): UpdateNewsletterEmailResult!
|
||||
updatePage(input: UpdatePageInput!): UpdatePageResult!
|
||||
updateSubscription(input: UpdateSubscriptionInput!): UpdateSubscriptionResult!
|
||||
updateUser(input: UpdateUserInput!): UpdateUserResult!
|
||||
|
|
@ -1173,7 +1280,10 @@ type NewsletterEmail {
|
|||
address: String!
|
||||
confirmationCode: String
|
||||
createdAt: Date!
|
||||
description: String
|
||||
folder: String!
|
||||
id: ID!
|
||||
name: String
|
||||
subscriptionCount: Int!
|
||||
}
|
||||
|
||||
|
|
@ -1292,6 +1402,7 @@ type Query {
|
|||
article(format: String, slug: String!, username: String!): ArticleResult!
|
||||
articleSavingRequest(id: ID, url: String): ArticleSavingRequestResult!
|
||||
deviceTokens: DeviceTokensResult!
|
||||
feeds(input: FeedsInput!): FeedsResult!
|
||||
filters: FiltersResult!
|
||||
getUserPersonalization: GetUserPersonalizationResult!
|
||||
groups: GroupsResult!
|
||||
|
|
@ -1303,11 +1414,12 @@ type Query {
|
|||
recentEmails: RecentEmailsResult!
|
||||
recentSearches: RecentSearchesResult!
|
||||
rules(enabled: Boolean): RulesResult!
|
||||
scanFeeds(input: ScanFeedsInput!): ScanFeedsResult!
|
||||
search(after: String, first: Int, format: String, includeContent: Boolean, query: String): SearchResult!
|
||||
sendInstallInstructions: SendInstallInstructionsResult!
|
||||
subscriptions(sort: SortParams, type: SubscriptionType): SubscriptionsResult!
|
||||
typeaheadSearch(first: Int, query: String!): TypeaheadSearchResult!
|
||||
updatesSince(after: String, first: Int, since: Date!, sort: SortParams): UpdatesSinceResult!
|
||||
updatesSince(after: String, first: Int, folder: String, since: Date!, sort: SortParams): UpdatesSinceResult!
|
||||
user(userId: ID, username: String): UserResult!
|
||||
users: UsersResult!
|
||||
validateUsername(username: String!): Boolean!
|
||||
|
|
@ -1604,6 +1716,7 @@ enum SaveErrorCode {
|
|||
|
||||
input SaveFileInput {
|
||||
clientRequestId: ID!
|
||||
folder: String
|
||||
labels: [CreateLabelInput!]
|
||||
source: String!
|
||||
state: ArticleSavingRequestStatus
|
||||
|
|
@ -1625,6 +1738,7 @@ input SaveFilterInput {
|
|||
category: String
|
||||
description: String
|
||||
filter: String!
|
||||
folder: String
|
||||
name: String!
|
||||
position: Int
|
||||
}
|
||||
|
|
@ -1637,6 +1751,7 @@ type SaveFilterSuccess {
|
|||
|
||||
input SavePageInput {
|
||||
clientRequestId: ID!
|
||||
folder: String
|
||||
labels: [CreateLabelInput!]
|
||||
originalContent: String!
|
||||
parseResult: ParseResult
|
||||
|
|
@ -1658,6 +1773,7 @@ type SaveSuccess {
|
|||
|
||||
input SaveUrlInput {
|
||||
clientRequestId: ID!
|
||||
folder: String
|
||||
labels: [CreateLabelInput!]
|
||||
locale: String
|
||||
publishedAt: Date
|
||||
|
|
@ -1668,6 +1784,25 @@ input SaveUrlInput {
|
|||
url: String!
|
||||
}
|
||||
|
||||
type ScanFeedsError {
|
||||
errorCodes: [ScanFeedsErrorCode!]!
|
||||
}
|
||||
|
||||
enum ScanFeedsErrorCode {
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
input ScanFeedsInput {
|
||||
opml: String
|
||||
url: String
|
||||
}
|
||||
|
||||
union ScanFeedsResult = ScanFeedsError | ScanFeedsSuccess
|
||||
|
||||
type ScanFeedsSuccess {
|
||||
feeds: [Feed!]!
|
||||
}
|
||||
|
||||
type SearchError {
|
||||
errorCodes: [SearchErrorCode!]!
|
||||
}
|
||||
|
|
@ -1686,16 +1821,20 @@ type SearchItem {
|
|||
contentReader: ContentReader!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
folder: String!
|
||||
highlights: [Highlight!]
|
||||
id: ID!
|
||||
image: String
|
||||
isArchived: Boolean!
|
||||
labels: [Label!]
|
||||
language: String
|
||||
links: JSON
|
||||
originalArticleUrl: String
|
||||
ownedByViewer: Boolean
|
||||
pageId: ID
|
||||
pageType: PageType!
|
||||
previewContent: String
|
||||
previewContentType: String
|
||||
publishedAt: Date
|
||||
quote: String
|
||||
readAt: Date
|
||||
|
|
@ -1844,6 +1983,7 @@ input SetIntegrationInput {
|
|||
importItemState: ImportItemState
|
||||
name: String!
|
||||
syncedAt: Date
|
||||
taskName: String
|
||||
token: String!
|
||||
type: IntegrationType
|
||||
}
|
||||
|
|
@ -1874,6 +2014,7 @@ input SetLabelsInput {
|
|||
labelIds: [ID!]
|
||||
labels: [CreateLabelInput!]
|
||||
pageId: ID!
|
||||
source: String
|
||||
}
|
||||
|
||||
union SetLabelsResult = SetLabelsError | SetLabelsSuccess
|
||||
|
|
@ -1963,6 +2104,7 @@ enum SetUserPersonalizationErrorCode {
|
|||
}
|
||||
|
||||
input SetUserPersonalizationInput {
|
||||
fields: JSON
|
||||
fontFamily: String
|
||||
fontSize: Int
|
||||
libraryLayoutType: String
|
||||
|
|
@ -2068,6 +2210,10 @@ enum SubscribeErrorCode {
|
|||
}
|
||||
|
||||
input SubscribeInput {
|
||||
autoAddToLibrary: Boolean
|
||||
fetchContent: Boolean
|
||||
folder: String
|
||||
isPrivate: Boolean
|
||||
subscriptionType: SubscriptionType
|
||||
url: String!
|
||||
}
|
||||
|
|
@ -2079,11 +2225,15 @@ type SubscribeSuccess {
|
|||
}
|
||||
|
||||
type Subscription {
|
||||
autoAddToLibrary: Boolean
|
||||
count: Int!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
fetchContent: Boolean!
|
||||
folder: String!
|
||||
icon: String
|
||||
id: ID!
|
||||
isPrivate: Boolean
|
||||
lastFetchedAt: Date
|
||||
name: String!
|
||||
newsletterEmail: String
|
||||
|
|
@ -2203,6 +2353,7 @@ input UpdateFilterInput {
|
|||
category: String
|
||||
description: String
|
||||
filter: String
|
||||
folder: String
|
||||
id: String!
|
||||
name: String
|
||||
position: Int
|
||||
|
|
@ -2307,6 +2458,28 @@ type UpdateLinkShareInfoSuccess {
|
|||
message: String!
|
||||
}
|
||||
|
||||
type UpdateNewsletterEmailError {
|
||||
errorCodes: [UpdateNewsletterEmailErrorCode!]!
|
||||
}
|
||||
|
||||
enum UpdateNewsletterEmailErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input UpdateNewsletterEmailInput {
|
||||
description: String
|
||||
folder: String
|
||||
id: ID!
|
||||
name: String
|
||||
}
|
||||
|
||||
union UpdateNewsletterEmailResult = UpdateNewsletterEmailError | UpdateNewsletterEmailSuccess
|
||||
|
||||
type UpdateNewsletterEmailSuccess {
|
||||
newsletterEmail: NewsletterEmail!
|
||||
}
|
||||
|
||||
type UpdatePageError {
|
||||
errorCodes: [UpdatePageErrorCode!]!
|
||||
}
|
||||
|
|
@ -2397,8 +2570,12 @@ enum UpdateSubscriptionErrorCode {
|
|||
}
|
||||
|
||||
input UpdateSubscriptionInput {
|
||||
autoAddToLibrary: Boolean
|
||||
description: String
|
||||
fetchContent: Boolean
|
||||
folder: String
|
||||
id: ID!
|
||||
isPrivate: Boolean
|
||||
lastFetchedAt: Date
|
||||
lastFetchedChecksum: String
|
||||
name: String
|
||||
|
|
@ -2557,6 +2734,7 @@ enum UserErrorCode {
|
|||
}
|
||||
|
||||
type UserPersonalization {
|
||||
fields: JSON
|
||||
fontFamily: String
|
||||
fontSize: Int
|
||||
id: ID
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemAndHighlightCrossRef
|
||||
import app.omnivore.omnivore.persistence.entities.saveHighlightChange
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -33,6 +38,8 @@ suspend fun DataService.createWebHighlight(jsonString: String, colorName: String
|
|||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
|
||||
val highlightChange = saveHighlightChange(db.highlightChangesDao(), createHighlightInput.articleId, highlight)
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = createHighlightInput.id,
|
||||
savedItemId = createHighlightInput.articleId
|
||||
|
|
@ -41,11 +48,7 @@ suspend fun DataService.createWebHighlight(jsonString: String, colorName: String
|
|||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
val newHighlight = networker.createHighlight(createHighlightInput)
|
||||
|
||||
newHighlight?.let {
|
||||
db.highlightDao().update(it)
|
||||
}
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +76,8 @@ suspend fun DataService.createNoteHighlight(savedItemId: String, note: String):
|
|||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
|
||||
val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = createHighlightId,
|
||||
savedItemId = savedItemId
|
||||
|
|
@ -81,21 +86,7 @@ suspend fun DataService.createNoteHighlight(savedItemId: String, note: String):
|
|||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
val newHighlight = networker.createHighlight(input = CreateHighlightParams(
|
||||
type = HighlightType.NOTE,
|
||||
articleId = savedItemId,
|
||||
id = createHighlightId,
|
||||
shortId = shortId,
|
||||
quote = null,
|
||||
patch = null,
|
||||
annotation = note,
|
||||
highlightPositionAnchorIndex = 0,
|
||||
highlightPositionPercent = 0.0
|
||||
).asCreateHighlightInput())
|
||||
|
||||
newHighlight?.let {
|
||||
db.highlightDao().update(it)
|
||||
}
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
|
||||
return createHighlightId
|
||||
|
|
@ -103,74 +94,84 @@ suspend fun DataService.createNoteHighlight(savedItemId: String, note: String):
|
|||
|
||||
suspend fun DataService.mergeWebHighlights(jsonString: String) {
|
||||
val mergeHighlightInput = Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput()
|
||||
Log.d("sync", "mergeHighlightInput: " + mergeHighlightInput.id + ": " + mergeHighlightInput)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = mergeHighlightInput.id) ?: return@withContext
|
||||
highlight.shortId = mergeHighlightInput.shortId
|
||||
highlight.quote = mergeHighlightInput.quote
|
||||
highlight.patch = mergeHighlightInput.patch
|
||||
highlight.prefix = mergeHighlightInput.prefix.getOrNull()
|
||||
highlight.annotation = mergeHighlightInput.annotation.getOrNull()
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
val highlight = Highlight(
|
||||
type = "HIGHLIGHT",
|
||||
highlightId = mergeHighlightInput.id,
|
||||
shortId = mergeHighlightInput.shortId,
|
||||
quote = mergeHighlightInput.quote,
|
||||
prefix = null,
|
||||
suffix = null,
|
||||
patch = mergeHighlightInput.patch,
|
||||
annotation = mergeHighlightInput.annotation.getOrNull(),
|
||||
createdAt = null,
|
||||
updatedAt = null,
|
||||
createdByMe = false,
|
||||
color = mergeHighlightInput.color.getOrNull(),
|
||||
highlightPositionPercent = mergeHighlightInput.highlightPositionPercent.getOrNull() ?: 0.0,
|
||||
highlightPositionAnchorIndex = mergeHighlightInput.highlightPositionAnchorIndex.getOrNull() ?: 0
|
||||
)
|
||||
|
||||
for (highlightID in mergeHighlightInput.overlapHighlightIdList) {
|
||||
deleteHighlight(highlightID)
|
||||
}
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
||||
|
||||
val highlightChange = saveHighlightChange(
|
||||
db.highlightChangesDao(),
|
||||
mergeHighlightInput.articleId,
|
||||
highlight,
|
||||
html = mergeHighlightInput.html.getOrNull(),
|
||||
overlappingIDs = mergeHighlightInput.overlapHighlightIdList
|
||||
)
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = mergeHighlightInput.id,
|
||||
savedItemId = mergeHighlightInput.articleId
|
||||
)
|
||||
|
||||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.updateWebHighlight(jsonString: String) {
|
||||
val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java).asUpdateHighlightInput()
|
||||
val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java)
|
||||
|
||||
if (updateHighlightParams.highlightId == null || updateHighlightParams.libraryItemId == null) {
|
||||
Log.d("error","ERROR INVALID HIGHLIGHT DATA")
|
||||
return
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = updateHighlightParams.highlightId) ?: return@withContext
|
||||
val highlight = db.highlightDao().findById(highlightId = updateHighlightParams.highlightId ?: "") ?: return@withContext
|
||||
|
||||
highlight.annotation = updateHighlightParams.annotation.getOrNull()
|
||||
highlight.annotation = updateHighlightParams.annotation
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
val isUpdatedOnServer = networker.updateHighlight(updateHighlightParams)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
highlight.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
val highlightChange = saveHighlightChange(db.highlightChangesDao(), updateHighlightParams.libraryItemId ?: "", highlight)
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.deleteHighlights(jsonString: String) {
|
||||
val highlightIDs = Gson().fromJson(jsonString, DeleteHighlightParams::class.java).asIdList()
|
||||
|
||||
for (highlightID in highlightIDs) {
|
||||
deleteHighlight(highlightID)
|
||||
}
|
||||
suspend fun DataService.deleteHighlightFromJSON(jsonString: String) {
|
||||
val deleteHighlightParams = Gson().fromJson(jsonString, DeleteHighlightParams::class.java)
|
||||
deleteHighlight(deleteHighlightParams.libraryItemId, deleteHighlightParams.highlightId)
|
||||
}
|
||||
|
||||
private suspend fun DataService.deleteHighlight(highlightID: String) {
|
||||
private suspend fun DataService.deleteHighlight(savedItemId: String, highlightID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = highlightID) ?: return@withContext
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
val highlight = db.highlightDao().findById(highlightId = highlightID)
|
||||
|
||||
val isUpdatedOnServer = networker.deleteHighlights(listOf(highlightID))
|
||||
highlight?.let {
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
db.highlightDao().deleteById(highlightId = highlightID)
|
||||
val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,48 @@
|
|||
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.*
|
||||
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
|
||||
|
||||
suspend fun DataService.startSyncChannels() {
|
||||
Log.d("sync", "Starting sync channels")
|
||||
for (savedItem in savedItemSyncChannel) {
|
||||
syncSavedItem(savedItem)
|
||||
}
|
||||
}
|
||||
|
||||
for (highlight in highlightSyncChannel) {
|
||||
syncHighlight(highlight)
|
||||
suspend fun DataService.performHighlightChange(highlightChange: HighlightChange) {
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
if (syncHighlightChange(highlightChange)) {
|
||||
db.highlightChangesDao().deleteById(highlight.highlightId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun DataService.syncOfflineItemsWithServerIfNeeded() {
|
||||
val unSyncedSavedItems = db.savedItemDao().getUnSynced()
|
||||
val unSyncedHighlights = db.highlightDao().getUnSynced()
|
||||
val unSyncedHighlights = db.highlightChangesDao().getUnSynced()
|
||||
|
||||
for (savedItem in unSyncedSavedItems) {
|
||||
delay(250)
|
||||
savedItemSyncChannel.send(savedItem)
|
||||
}
|
||||
|
||||
for (highlight in unSyncedHighlights) {
|
||||
delay(250)
|
||||
highlightSyncChannel.send(highlight)
|
||||
for (change in unSyncedHighlights) {
|
||||
performHighlightChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +94,9 @@ private suspend fun DataService.syncSavedItem(item: SavedItem) {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncHighlight(highlight: Highlight) {
|
||||
private suspend fun DataService.syncHighlightChange(highlightChange: HighlightChange): Boolean {
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
|
||||
fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
highlight.serverSyncStatus = status.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
|
|
@ -91,7 +105,6 @@ private suspend fun DataService.syncHighlight(highlight: Highlight) {
|
|||
when (highlight.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId))
|
||||
|
||||
if (isDeletedOnServer) {
|
||||
|
|
@ -99,7 +112,9 @@ private suspend fun DataService.syncHighlight(highlight: Highlight) {
|
|||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
return isDeletedOnServer != null
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
|
|
@ -116,30 +131,85 @@ private suspend fun DataService.syncHighlight(highlight: Highlight) {
|
|||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
return isUpdatedOnServer != null
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val savedItemID = db.savedItemAndHighlightCrossRefDao()
|
||||
.associatedSavedItemID(highlightId = highlight.highlightId)
|
||||
|
||||
val isCreatedOnServer = networker.createHighlight(
|
||||
CreateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
articleId = 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),
|
||||
)
|
||||
|
||||
if (isCreatedOnServer != null) {
|
||||
Log.d("sync", "Creating highlight from input: ${input}")
|
||||
val createResult = networker.createHighlight(
|
||||
input
|
||||
)
|
||||
if (createResult.newHighlight != null || createResult.alreadyExists) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
return true
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
return false
|
||||
}
|
||||
}
|
||||
else -> return
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import app.omnivore.omnivore.graphql.generated.CreateHighlightMutation
|
|||
import app.omnivore.omnivore.graphql.generated.DeleteHighlightMutation
|
||||
import app.omnivore.omnivore.graphql.generated.MergeHighlightMutation
|
||||
import app.omnivore.omnivore.graphql.generated.UpdateHighlightMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightErrorCode
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||
|
|
@ -39,6 +40,7 @@ data class CreateHighlightParams(
|
|||
|
||||
data class UpdateHighlightParams(
|
||||
val highlightId: String?,
|
||||
val libraryItemId: String?,
|
||||
val `annotation`: String?,
|
||||
val sharedAt: String?,
|
||||
) {
|
||||
|
|
@ -73,9 +75,10 @@ data class MergeHighlightsParams(
|
|||
}
|
||||
|
||||
data class DeleteHighlightParams(
|
||||
val highlightId: String?
|
||||
val highlightId: String,
|
||||
val libraryItemId: String
|
||||
) {
|
||||
fun asIdList() = listOf(highlightId ?: "")
|
||||
fun asIdList() = listOf(highlightId)
|
||||
}
|
||||
|
||||
suspend fun Networker.deleteHighlight(jsonString: String): Boolean {
|
||||
|
|
@ -107,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
|
||||
|
|
@ -134,18 +136,22 @@ suspend fun Networker.createWebHighlight(jsonString: String): Boolean {
|
|||
return createHighlight(input) != null
|
||||
}
|
||||
|
||||
suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
|
||||
Log.d("Loggo", "created highlight input: $input")
|
||||
data class CreateHighlightResult(
|
||||
val failedToCreate: Boolean,
|
||||
val alreadyExists: Boolean,
|
||||
val newHighlight: Highlight?
|
||||
)
|
||||
|
||||
suspend fun Networker.createHighlight(input: CreateHighlightInput): CreateHighlightResult {
|
||||
try {
|
||||
val result = authenticatedApolloClient().mutation(CreateHighlightMutation(input)).execute()
|
||||
Log.d("Loggo", "result: ${result.data}")
|
||||
|
||||
|
||||
val createdHighlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight
|
||||
|
||||
if (createdHighlight != null) {
|
||||
return Highlight(
|
||||
return CreateHighlightResult(
|
||||
failedToCreate = false,
|
||||
alreadyExists = false,
|
||||
newHighlight = Highlight(
|
||||
type = createdHighlight.highlightFields.type.toString(),
|
||||
highlightId = createdHighlight.highlightFields.id,
|
||||
shortId = createdHighlight.highlightFields.shortId,
|
||||
|
|
@ -161,10 +167,22 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
|
|||
highlightPositionPercent = createdHighlight.highlightFields.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = createdHighlight.highlightFields.highlightPositionAnchorIndex
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
if (result.data?.createHighlight?.onCreateHighlightError?.errorCodes?.first() == CreateHighlightErrorCode.ALREADY_EXISTS) {
|
||||
return CreateHighlightResult(
|
||||
failedToCreate = false,
|
||||
alreadyExists = true,
|
||||
newHighlight = null
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
return null
|
||||
Log.d("sync", "error creating highlight: " +e)
|
||||
}
|
||||
return CreateHighlightResult(
|
||||
failedToCreate = true,
|
||||
alreadyExists = false,
|
||||
newHighlight = null
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ suspend fun Networker.savedItemUpdates(
|
|||
try {
|
||||
val result = authenticatedApolloClient().query(
|
||||
UpdatesSinceQuery(
|
||||
folder = Optional.presentIfNotNull("all"),
|
||||
after = Optional.presentIfNotNull(cursor),
|
||||
first = Optional.presentIfNotNull(limit),
|
||||
since = since
|
||||
|
|
|
|||
|
|
@ -10,15 +10,17 @@ import app.omnivore.omnivore.persistence.entities.*
|
|||
SavedItem::class,
|
||||
SavedItemLabel::class,
|
||||
Highlight::class,
|
||||
HighlightChange::class,
|
||||
SavedItemAndSavedItemLabelCrossRef::class,
|
||||
SavedItemAndHighlightCrossRef::class
|
||||
],
|
||||
version = 15
|
||||
version = 24
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun viewerDao(): ViewerDao
|
||||
abstract fun savedItemDao(): SavedItemDao
|
||||
abstract fun highlightDao(): HighlightDao
|
||||
abstract fun highlightChangesDao(): HighlightChangesDao
|
||||
abstract fun savedItemLabelDao(): SavedItemLabelDao
|
||||
abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao
|
||||
abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
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,
|
||||
|
||||
val type: String,
|
||||
var annotation: String?,
|
||||
val createdAt: String?,
|
||||
val createdByMe: Boolean = true,
|
||||
val markedForDeletion: Boolean = false,
|
||||
var patch: String?,
|
||||
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 overlappingIDs: List<String>?
|
||||
)
|
||||
|
||||
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,
|
||||
type = highlight.type,
|
||||
shortId = highlight.shortId,
|
||||
quote = highlight.quote,
|
||||
prefix = highlight.prefix,
|
||||
suffix = highlight.suffix,
|
||||
patch = highlight.patch,
|
||||
html = html,
|
||||
annotation = highlight.annotation,
|
||||
createdAt = highlight.createdAt,
|
||||
updatedAt = highlight.updatedAt,
|
||||
createdByMe = highlight.createdByMe,
|
||||
color =highlight.color,
|
||||
highlightPositionPercent = highlight.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = highlight.highlightPositionAnchorIndex,
|
||||
serverSyncStatus = highlight.serverSyncStatus,
|
||||
overlappingIDs = overlappingIDs
|
||||
)
|
||||
dao.insertAll(listOf(change))
|
||||
return change
|
||||
}
|
||||
|
||||
fun highlightChangeToHighlight(change: HighlightChange): Highlight {
|
||||
return Highlight(
|
||||
highlightId = change.highlightId,
|
||||
type = change.type,
|
||||
shortId = change.shortId,
|
||||
quote = change.quote,
|
||||
prefix = change.prefix,
|
||||
suffix = change.suffix,
|
||||
patch = change.patch,
|
||||
annotation = change.annotation,
|
||||
createdAt = change.createdAt,
|
||||
updatedAt = change.updatedAt,
|
||||
createdByMe = change.createdByMe,
|
||||
color = change.color,
|
||||
highlightPositionPercent = change.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = change.highlightPositionAnchorIndex,
|
||||
serverSyncStatus = change.serverSyncStatus
|
||||
)
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface HighlightChangesDao {
|
||||
@Query("SELECT * FROM highlightChange WHERE serverSyncStatus != 0 ORDER BY updatedAt ASC")
|
||||
fun getUnSynced(): List<HighlightChange>
|
||||
|
||||
@Query("DELETE FROM highlightChange WHERE highlightId = :highlightId")
|
||||
fun deleteById(highlightId: String)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<HighlightChange>)
|
||||
}
|
||||
|
||||
|
|
@ -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)) },
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import app.omnivore.omnivore.R
|
|||
import app.omnivore.omnivore.ui.save.SaveState
|
||||
import app.omnivore.omnivore.ui.save.SaveViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddLinkSheetContent(
|
||||
viewModel: SaveViewModel,
|
||||
|
|
@ -84,42 +85,47 @@ fun AddLinkSheetContent(
|
|||
viewModel.saveURL(url)
|
||||
}
|
||||
|
||||
Surface(
|
||||
androidx.compose.material.Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.add_link_sheet_title))
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
),
|
||||
navigationIcon = {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = stringResource(R.string.label_selection_sheet_action_cancel))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
TextButton(onClick = { addLink(textFieldValue.text) }) {
|
||||
Text(stringResource(R.string.add_link_sheet_action_add_link))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 5.dp)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(horizontal = 10.dp)
|
||||
) {
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = stringResource(R.string.add_link_sheet_action_cancel))
|
||||
}
|
||||
|
||||
Text(stringResource(R.string.add_link_sheet_title), fontWeight = FontWeight.ExtraBold)
|
||||
|
||||
TextButton(onClick = { addLink(textFieldValue.text) }) {
|
||||
Text(stringResource(R.string.add_link_sheet_action_add_link))
|
||||
}
|
||||
}
|
||||
|
||||
if (isSaving.value == true) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.width(16.dp),
|
||||
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
|
@ -129,18 +135,29 @@ fun AddLinkSheetContent(
|
|||
value = textFieldValue,
|
||||
placeholder = { Text(stringResource(R.string.add_link_sheet_text_field_placeholder)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||
leadingIcon = { Icon(imageVector = Icons.Default.Link, contentDescription = "linkIcon") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Link,
|
||||
contentDescription = "linkIcon"
|
||||
)
|
||||
},
|
||||
onValueChange = { textFieldValue = it },
|
||||
modifier = Modifier.focusRequester(focusRequester).padding(top = 24.dp).fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.padding(top = 24.dp)
|
||||
.padding(horizontal = 10.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
if (clipboardText != null) {
|
||||
Button(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
modifier = Modifier.padding(top = 10.dp) .padding(horizontal = 10.dp)
|
||||
,
|
||||
onClick = {
|
||||
textFieldValue = TextFieldValue(
|
||||
text = clipboardText,
|
||||
selection = TextRange(clipboardText.length))
|
||||
selection = TextRange(clipboardText.length)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.add_link_sheet_action_paste_from_clipboard))
|
||||
|
|
|
|||
|
|
@ -3,15 +3,47 @@
|
|||
package app.omnivore.omnivore.ui.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircle
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -20,8 +52,12 @@ import androidx.compose.ui.focus.FocusRequester
|
|||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.platform.ViewConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
|
|
@ -29,11 +65,15 @@ import androidx.compose.ui.text.toLowerCase
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import com.dokar.chiptextfield.*
|
||||
import com.dokar.chiptextfield.Chip
|
||||
import com.dokar.chiptextfield.ChipTextField
|
||||
import com.dokar.chiptextfield.ChipTextFieldDefaults
|
||||
import com.dokar.chiptextfield.ChipTextFieldState
|
||||
import com.dokar.chiptextfield.rememberChipTextFieldState
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import java.util.*
|
||||
|
||||
|
||||
//@Composable
|
||||
|
|
@ -99,216 +139,233 @@ import java.util.*
|
|||
//}
|
||||
|
||||
@Composable
|
||||
fun CircleIcon(colorHex: String){
|
||||
val chipColors = LabelChipColors.fromHex(colorHex)
|
||||
val viewConfiguration = LocalViewConfiguration.current
|
||||
val viewConfigurationOverride = remember(viewConfiguration) {
|
||||
ViewConfigurationOverride(
|
||||
base = viewConfiguration,
|
||||
minimumTouchTargetSize = DpSize(24.dp, 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalViewConfiguration provides viewConfigurationOverride) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp, end = 2.dp)
|
||||
.padding(vertical = 7.dp)
|
||||
) {
|
||||
Canvas(modifier = Modifier.size(12.dp), onDraw = {
|
||||
drawCircle(color = chipColors.containerColor)
|
||||
})
|
||||
fun CircleIcon(colorHex: String) {
|
||||
val chipColors = LabelChipColors.fromHex(colorHex)
|
||||
val viewConfiguration = LocalViewConfiguration.current
|
||||
val viewConfigurationOverride = remember(viewConfiguration) {
|
||||
ViewConfigurationOverride(
|
||||
base = viewConfiguration,
|
||||
minimumTouchTargetSize = DpSize(24.dp, 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalViewConfiguration provides viewConfigurationOverride) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp, end = 2.dp)
|
||||
.padding(vertical = 7.dp)
|
||||
) {
|
||||
Canvas(modifier = Modifier.size(12.dp), onDraw = {
|
||||
drawCircle(color = chipColors.containerColor)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T : Chip> CloseButton(
|
||||
state: ChipTextFieldState<T>,
|
||||
chip: T,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = Color.Transparent,
|
||||
strokeColor: Color = Color.White,
|
||||
startPadding: Dp = 0.dp,
|
||||
endPadding: Dp = 4.dp
|
||||
state: ChipTextFieldState<T>,
|
||||
chip: T,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = Color.Transparent,
|
||||
strokeColor: Color = Color.White,
|
||||
startPadding: Dp = 0.dp,
|
||||
endPadding: Dp = 4.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(start = startPadding, end = endPadding)
|
||||
) {
|
||||
CloseButtonImpl(
|
||||
onClick = { state.removeChip(chip) },
|
||||
backgroundColor = backgroundColor,
|
||||
strokeColor = strokeColor
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(start = startPadding, end = endPadding)
|
||||
) {
|
||||
CloseButtonImpl(
|
||||
onClick = { state.removeChip(chip) },
|
||||
backgroundColor = backgroundColor,
|
||||
strokeColor = strokeColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ViewConfigurationOverride(
|
||||
base: ViewConfiguration,
|
||||
override val doubleTapMinTimeMillis: Long = base.doubleTapMinTimeMillis,
|
||||
override val doubleTapTimeoutMillis: Long = base.doubleTapTimeoutMillis,
|
||||
override val longPressTimeoutMillis: Long = base.longPressTimeoutMillis,
|
||||
override val touchSlop: Float = base.touchSlop,
|
||||
override val minimumTouchTargetSize: DpSize = base.minimumTouchTargetSize
|
||||
base: ViewConfiguration,
|
||||
override val doubleTapMinTimeMillis: Long = base.doubleTapMinTimeMillis,
|
||||
override val doubleTapTimeoutMillis: Long = base.doubleTapTimeoutMillis,
|
||||
override val longPressTimeoutMillis: Long = base.longPressTimeoutMillis,
|
||||
override val touchSlop: Float = base.touchSlop,
|
||||
override val minimumTouchTargetSize: DpSize = base.minimumTouchTargetSize
|
||||
) : ViewConfiguration
|
||||
|
||||
@Composable
|
||||
private fun CloseButtonImpl(
|
||||
onClick: () -> Unit,
|
||||
backgroundColor: Color,
|
||||
strokeColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
backgroundColor: Color,
|
||||
strokeColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val padding = with(LocalDensity.current) { 6.dp.toPx() }
|
||||
val strokeWidth = with(LocalDensity.current) { 1.2.dp.toPx() }
|
||||
val viewConfiguration = LocalViewConfiguration.current
|
||||
val viewConfigurationOverride = remember(viewConfiguration) {
|
||||
ViewConfigurationOverride(
|
||||
base = viewConfiguration,
|
||||
minimumTouchTargetSize = DpSize(24.dp, 24.dp)
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(LocalViewConfiguration provides viewConfigurationOverride) {
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.size(18.dp)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor)
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
drawLine(
|
||||
color = strokeColor,
|
||||
start = Offset(padding, padding),
|
||||
end = Offset(size.width - padding, size.height - padding),
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
drawLine(
|
||||
color = strokeColor,
|
||||
start = Offset(padding, size.height - padding),
|
||||
end = Offset(size.width - padding, padding),
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
val padding = with(LocalDensity.current) { 6.dp.toPx() }
|
||||
val strokeWidth = with(LocalDensity.current) { 1.2.dp.toPx() }
|
||||
val viewConfiguration = LocalViewConfiguration.current
|
||||
val viewConfigurationOverride = remember(viewConfiguration) {
|
||||
ViewConfigurationOverride(
|
||||
base = viewConfiguration,
|
||||
minimumTouchTargetSize = DpSize(24.dp, 24.dp)
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(LocalViewConfiguration provides viewConfigurationOverride) {
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.size(18.dp)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor)
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
drawLine(
|
||||
color = strokeColor,
|
||||
start = Offset(padding, padding),
|
||||
end = Offset(size.width - padding, size.height - padding),
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
drawLine(
|
||||
color = strokeColor,
|
||||
start = Offset(padding, size.height - padding),
|
||||
end = Offset(size.width - padding, padding),
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LabelChipView(label: SavedItemLabel) : Chip(label.name) {
|
||||
val label = label
|
||||
val label = label
|
||||
}
|
||||
|
||||
fun findOrCreateLabel(labelsViewModel: LabelsViewModel, labels: List<SavedItemLabel>, name: String): SavedItemLabel {
|
||||
val found = labels.find { it.name == name }
|
||||
if (found != null) {
|
||||
return found
|
||||
}
|
||||
return labelsViewModel.createNewSavedItemLabelWithTemp(name, LabelSwatchHelper.random())
|
||||
fun findOrCreateLabel(
|
||||
labelsViewModel: LabelsViewModel,
|
||||
labels: List<SavedItemLabel>,
|
||||
name: String
|
||||
): SavedItemLabel {
|
||||
val found = labels.find { it.name == name }
|
||||
if (found != null) {
|
||||
return found
|
||||
}
|
||||
return labelsViewModel.createNewSavedItemLabelWithTemp(name, LabelSwatchHelper.random())
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class,
|
||||
ExperimentalMaterial3Api::class
|
||||
@OptIn(
|
||||
ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class,
|
||||
ExperimentalMaterial3Api::class
|
||||
)
|
||||
fun LabelsSelectionSheetContent(
|
||||
isLibraryMode: Boolean,
|
||||
labels: List<SavedItemLabel>,
|
||||
initialSelectedLabels: List<SavedItemLabel>,
|
||||
labelsViewModel: LabelsViewModel,
|
||||
onCancel: () -> Unit,
|
||||
onSave: (List<SavedItemLabel>) -> Unit,
|
||||
onCreateLabel: (String, String) -> Unit
|
||||
isLibraryMode: Boolean,
|
||||
labels: List<SavedItemLabel>,
|
||||
initialSelectedLabels: List<SavedItemLabel>,
|
||||
labelsViewModel: LabelsViewModel,
|
||||
onCancel: () -> Unit,
|
||||
onSave: (List<SavedItemLabel>) -> Unit,
|
||||
onCreateLabel: (String, String) -> Unit
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
val state = rememberChipTextFieldState(initialSelectedLabels.map {
|
||||
LabelChipView(it)
|
||||
})
|
||||
val state = rememberChipTextFieldState(initialSelectedLabels.map {
|
||||
LabelChipView(it)
|
||||
})
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var filterTextValue by remember { mutableStateOf(TextFieldValue()) }
|
||||
val onFilterTextValueChange: (TextFieldValue) -> Unit = { filterTextValue = it }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var filterTextValue by remember { mutableStateOf(TextFieldValue()) }
|
||||
val onFilterTextValueChange: (TextFieldValue) -> Unit = { filterTextValue = it }
|
||||
|
||||
val filteredLabels = labels.filter { label ->
|
||||
val text = filterTextValue.text.toLowerCase(Locale.current)
|
||||
val result = (text.isEmpty() || label.name.toLowerCase(Locale.current).startsWith(text))
|
||||
val alreadySelected = state.chips.map { it.label.name }.contains(label.name)
|
||||
result && !alreadySelected
|
||||
}
|
||||
val filteredLabels = labels.filter { label ->
|
||||
val text = filterTextValue.text.toLowerCase(Locale.current)
|
||||
val result = (text.isEmpty() || label.name.toLowerCase(Locale.current).startsWith(text))
|
||||
val alreadySelected = state.chips.map { it.label.name }.contains(label.name)
|
||||
result && !alreadySelected
|
||||
}
|
||||
|
||||
val currentLabel = labels.find {
|
||||
val text = filterTextValue.text.toLowerCase(Locale.current)
|
||||
it.name.toLowerCase(Locale.current) == text
|
||||
}
|
||||
val currentLabel = labels.find {
|
||||
val text = filterTextValue.text.toLowerCase(Locale.current)
|
||||
it.name.toLowerCase(Locale.current) == text
|
||||
}
|
||||
|
||||
val titleText = if (isLibraryMode)
|
||||
stringResource(R.string.label_selection_sheet_title) else
|
||||
stringResource(R.string.label_selection_sheet_title_alt)
|
||||
val titleText = if (isLibraryMode)
|
||||
stringResource(R.string.label_selection_sheet_title) else
|
||||
stringResource(R.string.label_selection_sheet_title_alt)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 5.dp)
|
||||
) {
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = stringResource(R.string.label_selection_sheet_action_cancel))
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(titleText)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
),
|
||||
navigationIcon = {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = stringResource(R.string.label_selection_sheet_action_cancel))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
TextButton(onClick = { onSave(state.chips.map { it.label }) }) {
|
||||
Text(
|
||||
text = if (isLibraryMode)
|
||||
stringResource(R.string.label_selection_sheet_action_search) else
|
||||
stringResource(R.string.label_selection_sheet_action_save)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Text(titleText, fontWeight = FontWeight.ExtraBold)
|
||||
|
||||
TextButton(onClick = { onSave(state.chips.map { it.label }) }) {
|
||||
Text(text = if (isLibraryMode)
|
||||
stringResource(R.string.label_selection_sheet_action_search) else
|
||||
stringResource(R.string.label_selection_sheet_action_save))
|
||||
}
|
||||
}
|
||||
|
||||
ChipTextField(
|
||||
state = state,
|
||||
value = filterTextValue,
|
||||
onValueChange = onFilterTextValueChange,
|
||||
onSubmit = {
|
||||
if (isLibraryMode) {
|
||||
currentLabel?.let {
|
||||
LabelChipView(it)
|
||||
}
|
||||
} else {
|
||||
LabelChipView(findOrCreateLabel(labelsViewModel = labelsViewModel, labels = labels, name = it.text))
|
||||
}
|
||||
},
|
||||
chipLeadingIcon = { chip -> CircleIcon(colorHex = chip.label.color) },
|
||||
chipTrailingIcon = { chip -> CloseButton(state, chip) },
|
||||
interactionSource = interactionSource,
|
||||
chipStyle = ChipTextFieldDefaults.chipStyle(
|
||||
shape = androidx.compose.material.MaterialTheme.shapes.medium,
|
||||
unfocusedBorderWidth = 0.dp,
|
||||
focusedTextColor = Color(0xFFAEAEAF),
|
||||
focusedBorderColor = Color(0xFF2A2A2A),
|
||||
focusedBackgroundColor = Color(0xFF2A2A2A)
|
||||
),
|
||||
colors = androidx.compose.material.TextFieldDefaults.textFieldColors(
|
||||
textColor = Color(0xFFAEAEAF),
|
||||
backgroundColor = Color(0xFF3D3D3D)
|
||||
),
|
||||
contentPadding = PaddingValues(10.dp),
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 45.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
.focusRequester(focusRequester)
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(horizontal = 10.dp)
|
||||
) {
|
||||
ChipTextField(
|
||||
state = state,
|
||||
value = filterTextValue,
|
||||
onValueChange = onFilterTextValueChange,
|
||||
onSubmit = {
|
||||
if (isLibraryMode) {
|
||||
currentLabel?.let {
|
||||
LabelChipView(it)
|
||||
}
|
||||
} else {
|
||||
LabelChipView(
|
||||
findOrCreateLabel(
|
||||
labelsViewModel = labelsViewModel,
|
||||
labels = labels,
|
||||
name = it.text
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
chipLeadingIcon = { chip -> CircleIcon(colorHex = chip.label.color) },
|
||||
chipTrailingIcon = { chip -> CloseButton(state, chip) },
|
||||
interactionSource = interactionSource,
|
||||
chipStyle = ChipTextFieldDefaults.chipStyle(
|
||||
shape = androidx.compose.material.MaterialTheme.shapes.medium,
|
||||
unfocusedBorderWidth = 0.dp,
|
||||
focusedTextColor = Color(0xFFAEAEAF),
|
||||
focusedBorderColor = Color(0xFF2A2A2A),
|
||||
focusedBackgroundColor = Color(0xFF2A2A2A)
|
||||
),
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
textColor = MaterialTheme.colorScheme.onBackground,
|
||||
backgroundColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
contentPadding = PaddingValues(10.dp),
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 45.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp)
|
||||
.padding(horizontal = 10.dp)
|
||||
.focusRequester(focusRequester)
|
||||
// .onFocusEvent {
|
||||
// val text = filterTextValue.text
|
||||
// if (it.hasFocus) {
|
||||
|
|
@ -316,78 +373,92 @@ fun LabelsSelectionSheetContent(
|
|||
// onFilterTextValueChange(filterTextValue.copy(selection = TextRange(selection)))
|
||||
// }
|
||||
// }
|
||||
)
|
||||
|
||||
if (!isLibraryMode && filterTextValue.text.isNotEmpty() && currentLabel == null) {
|
||||
val context = LocalContext.current
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val labelName = filterTextValue.text.trim()
|
||||
when(labelsViewModel.validateLabelName(labelName)) {
|
||||
LabelsViewModel.Error.LabelNameTooLong -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.label_selection_sheet_label_too_long_error_msg,
|
||||
labelsViewModel.labelNameMaxLength),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
null -> {
|
||||
val label = findOrCreateLabel(
|
||||
labelsViewModel = labelsViewModel,
|
||||
labels = labels,
|
||||
name = labelName
|
||||
)
|
||||
|
||||
state.addChip(LabelChipView(label))
|
||||
filterTextValue = TextFieldValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 10.dp)
|
||||
.padding(top = 10.dp, bottom = 5.dp)
|
||||
)
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AddCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(text = stringResource(R.string.label_selection_sheet_text_create, filterTextValue.text.trim()))
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredLabels.isNotEmpty()) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(10.dp)
|
||||
.padding(bottom = 55.dp)
|
||||
) {
|
||||
filteredLabels.forEach { label ->
|
||||
val chipColors = LabelChipColors.fromHex(label.color)
|
||||
|
||||
LabelChip(
|
||||
name = label.name,
|
||||
colors = chipColors,
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp, bottom = 10.dp)
|
||||
.clickable {
|
||||
state.addChip(LabelChipView(label))
|
||||
filterTextValue = TextFieldValue()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!isLibraryMode && filterTextValue.text.isNotEmpty() && currentLabel == null) {
|
||||
val context = LocalContext.current
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val labelName = filterTextValue.text.trim()
|
||||
when (labelsViewModel.validateLabelName(labelName)) {
|
||||
LabelsViewModel.Error.LabelNameTooLong -> {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(
|
||||
R.string.label_selection_sheet_label_too_long_error_msg,
|
||||
labelsViewModel.labelNameMaxLength
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
|
||||
null -> {
|
||||
val label = findOrCreateLabel(
|
||||
labelsViewModel = labelsViewModel,
|
||||
labels = labels,
|
||||
name = labelName
|
||||
)
|
||||
|
||||
state.addChip(LabelChipView(label))
|
||||
filterTextValue = TextFieldValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 10.dp)
|
||||
.padding(top = 24.dp, bottom = 5.dp)
|
||||
)
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AddCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.label_selection_sheet_text_create,
|
||||
filterTextValue.text.trim()
|
||||
),
|
||||
style = TextStyle(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredLabels.isNotEmpty()) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(10.dp)
|
||||
.padding(bottom = 55.dp)
|
||||
) {
|
||||
filteredLabels.forEach { label ->
|
||||
val chipColors = LabelChipColors.fromHex(label.color)
|
||||
|
||||
LabelChip(
|
||||
name = label.name,
|
||||
colors = chipColors,
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp, bottom = 10.dp)
|
||||
.clickable {
|
||||
state.addChip(LabelChipView(label))
|
||||
filterTextValue = TextFieldValue()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
state.focusTextField()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
state.focusTextField()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EditInfoSheetContent(
|
||||
savedItemId: String?,
|
||||
|
|
@ -68,44 +69,47 @@ fun EditInfoSheetContent(
|
|||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
androidx.compose.material.Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.edit_info_sheet_title))
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
),
|
||||
navigationIcon = {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = stringResource(R.string.edit_info_sheet_action_cancel))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
TextButton(onClick = {
|
||||
val newTitle = titleTextFieldValue.text
|
||||
val newAuthor = authorTextFieldValue.text.ifEmpty { null }
|
||||
val newDescription = descriptionTextFieldValue.text.ifEmpty { null }
|
||||
|
||||
savedItemId?.let {
|
||||
viewModel.editInfo(it, newTitle, newAuthor, newDescription)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.edit_info_sheet_action_save))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(horizontal = 10.dp)
|
||||
) {
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = stringResource(R.string.edit_info_sheet_action_cancel))
|
||||
}
|
||||
|
||||
Text(stringResource(R.string.edit_info_sheet_title), fontWeight = FontWeight.ExtraBold)
|
||||
|
||||
TextButton(onClick = {
|
||||
val newTitle = titleTextFieldValue.text
|
||||
val newAuthor = authorTextFieldValue.text.ifEmpty { null }
|
||||
val newDescription = descriptionTextFieldValue.text.ifEmpty { null }
|
||||
|
||||
savedItemId?.let {
|
||||
viewModel.editInfo(it, newTitle, newAuthor, newDescription)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.edit_info_sheet_action_save))
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdating.value == true) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.material.DismissValue
|
|||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.FractionalThreshold
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.SwipeToDismiss
|
||||
|
|
@ -31,6 +32,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
|||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material.rememberDismissState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -144,14 +146,21 @@ fun showAddLinkBottomSheet(libraryViewModel: LibraryViewModel) {
|
|||
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.ADD_LINK
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun LabelBottomSheet(
|
||||
libraryViewModel: LibraryViewModel,
|
||||
labelsViewModel: LabelsViewModel,
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
ModalBottomSheet(onDismissRequest = { onDismiss() }) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onDismiss() },
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
),
|
||||
) {
|
||||
|
||||
val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit()
|
||||
val labels: List<SavedItemLabel> by libraryViewModel.savedItemLabelsLiveData.observeAsState(
|
||||
listOf()
|
||||
|
|
@ -206,7 +215,14 @@ fun AddLinkBottomSheet(
|
|||
saveViewModel: SaveViewModel,
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
ModalBottomSheet(onDismissRequest = { onDismiss() }) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onDismiss() },
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
),
|
||||
) {
|
||||
|
||||
AddLinkSheetContent(
|
||||
viewModel = saveViewModel,
|
||||
onCancel = {
|
||||
|
|
@ -228,7 +244,13 @@ fun EditBottomSheet(
|
|||
libraryViewModel: LibraryViewModel,
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
ModalBottomSheet(onDismissRequest = { onDismiss() }) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onDismiss() },
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
),
|
||||
) {
|
||||
val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit()
|
||||
EditInfoSheetContent(
|
||||
savedItemId = currentSavedItemData?.savedItem?.savedItemId,
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ class LibraryViewModel @Inject constructor(
|
|||
|
||||
SavedItemAction.EditLabels -> {
|
||||
currentItemLiveData.value = itemID
|
||||
bottomSheetState.value = LibraryBottomSheetState.EDIT
|
||||
bottomSheetState.value = LibraryBottomSheetState.LABEL
|
||||
}
|
||||
|
||||
SavedItemAction.EditInfo -> {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
|
||||
val selectedWebFontName = remember { mutableStateOf(currentWebPreferences.fontFamily.displayText) }
|
||||
|
||||
|
||||
var fontSizeSliderValue by remember { mutableStateOf(currentWebPreferences.textFontSize.toFloat()) }
|
||||
var marginSliderValue by remember { mutableStateOf(currentWebPreferences.maxWidthPercentage.toFloat()) }
|
||||
var lineSpacingSliderValue by remember { mutableStateOf(currentWebPreferences.lineHeight.toFloat()) }
|
||||
|
|
@ -112,8 +111,8 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
fontSizeSliderValue = it
|
||||
webReaderViewModel.setFontSize(it.toInt())
|
||||
},
|
||||
steps = 10,
|
||||
valueRange = 10f..48f,
|
||||
steps = 40,
|
||||
valueRange = 10f..50f,
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.reader_preferences_view_margin), style = TextStyle(
|
||||
|
|
@ -127,7 +126,7 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
marginSliderValue = it
|
||||
webReaderViewModel.setMaxWidthPercentage(it.toInt())
|
||||
},
|
||||
steps = 4,
|
||||
steps = 40,
|
||||
valueRange = 60f..100f,
|
||||
)
|
||||
|
||||
|
|
@ -142,7 +141,7 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
lineSpacingSliderValue = it
|
||||
webReaderViewModel.setLineHeight(it.toInt())
|
||||
},
|
||||
steps = 8,
|
||||
steps = 50,
|
||||
valueRange = 100f..300f,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
|
|
|
|||
|
|
@ -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.deleteHighlights(jsonString)
|
||||
dataService.deleteHighlightFromJSON(jsonString)
|
||||
}
|
||||
}
|
||||
"updateHighlight" -> {
|
||||
Log.d("Loggo", "receive update highlight action: $jsonString")
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebHighlight(jsonString)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
|
|
@ -64,9 +66,12 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, settingsViewModel: Setti
|
|||
Box(
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
|
||||
val version = "Omnivore Version: " + BuildConfig.VERSION_NAME
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
|
|
@ -74,23 +79,6 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, settingsViewModel: Setti
|
|||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
|
||||
// profile pic and name
|
||||
|
||||
// SettingRow(text = "Labels") { Log.d("settings", "labels button tapped") }
|
||||
// RowDivider()
|
||||
// SettingRow(text = "Emails") { Log.d("settings", "emails button tapped") }
|
||||
// RowDivider()
|
||||
// SettingRow(text = "Subscriptions") { Log.d("settings", "subscriptions button tapped") }
|
||||
// RowDivider()
|
||||
// SettingRow(text = "Clubs") { Log.d("settings", "clubs button tapped") }
|
||||
|
||||
// SectionSpacer()
|
||||
|
||||
// SettingRow(text = "Push Notifications") { Log.d("settings", "pn button tapped") }
|
||||
// RowDivider()
|
||||
// SettingRow(text = "Text to Speech") { Log.d("settings", "tts button tapped") }
|
||||
//
|
||||
// SectionSpacer()
|
||||
|
||||
SettingRow(text = stringResource(R.string.settings_view_setting_row_documentation)) {
|
||||
navController.navigate(Routes.Documentation.route)
|
||||
|
|
@ -118,6 +106,13 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, settingsViewModel: Setti
|
|||
showLogoutDialog.value = true
|
||||
}
|
||||
RowDivider()
|
||||
|
||||
Text(
|
||||
text = version,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier
|
||||
.padding(15.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (showLogoutDialog.value) {
|
||||
|
|
|
|||
|
|
@ -2,63 +2,67 @@ package app.omnivore.omnivore.ui.theme
|
|||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
//
|
||||
val md_theme_light_primary = Color(0xFF745B00)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
//val md_theme_light_primaryContainer = Color(0xFFFFE08C)
|
||||
//val md_theme_light_onPrimaryContainer = Color(0xFF241A00)
|
||||
//val md_theme_light_secondary = Color(0xFF6D5E00)
|
||||
//val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
//val md_theme_light_secondaryContainer = Color(0xFFFCE365)
|
||||
//val md_theme_light_onSecondaryContainer = Color(0xFF211B00)
|
||||
//val md_theme_light_tertiary = Color(0xFF4B670A)
|
||||
//val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
//val md_theme_light_tertiaryContainer = Color(0xFFCBEF86)
|
||||
//val md_theme_light_onTertiaryContainer = Color(0xFF141F00)
|
||||
//val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
//val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
//val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
//val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
//val md_theme_light_background = Color(0xFFF7FFED)
|
||||
//val md_theme_light_onBackground = Color(0xFF032100)
|
||||
//val md_theme_light_surface = Color(0xFFF7FFED)
|
||||
//val md_theme_light_onSurface = Color(0xFF032100)
|
||||
//val md_theme_light_surfaceVariant = Color(0xFFEBE1CF)
|
||||
//val md_theme_light_onSurfaceVariant = Color(0xFF4C4639)
|
||||
//val md_theme_light_outline = Color(0xFF7E7667)
|
||||
//val md_theme_light_inverseOnSurface = Color(0xFFCBFFB5)
|
||||
//val md_theme_light_inverseSurface = Color(0xFF083900)
|
||||
//val md_theme_light_inversePrimary = Color(0xFFEFC125)
|
||||
//val md_theme_light_shadow = Color(0xFF000000)
|
||||
//val md_theme_light_surfaceTint = Color(0xFF745B00)
|
||||
//
|
||||
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
|
||||
val md_theme_light_secondary = Color(0xFF6F5B40)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF271904)
|
||||
val md_theme_light_tertiary = Color(0xFF51643F)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF102004)
|
||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
val md_theme_light_onBackground = Color(0xFF1F1B16)
|
||||
val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
val md_theme_light_onSurface = Color(0xFF1F1B16)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
|
||||
val md_theme_light_outline = Color(0xFF817567)
|
||||
val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
|
||||
val md_theme_light_inverseSurface = Color(0xFF34302A)
|
||||
val md_theme_light_inversePrimary = Color(0xFFFFB951)
|
||||
val md_theme_light_shadow = Color(0xFF000000)
|
||||
val md_theme_light_surfaceTint = Color(0xFF825500)
|
||||
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
|
||||
val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
val md_theme_dark_primary = Color(0xFFEFC125)
|
||||
val md_theme_dark_onPrimary = Color(0xFF3D2F00)
|
||||
//val md_theme_dark_primaryContainer = Color(0xFF584400)
|
||||
//val md_theme_dark_onPrimaryContainer = Color(0xFFFFE08C)
|
||||
//val md_theme_dark_secondary = Color(0xFFDEC64C)
|
||||
//val md_theme_dark_onSecondary = Color(0xFF393000)
|
||||
//val md_theme_dark_secondaryContainer = Color(0xFF524600)
|
||||
//val md_theme_dark_onSecondaryContainer = Color(0xFFFCE365)
|
||||
//val md_theme_dark_tertiary = Color(0xFFB0D36D)
|
||||
//val md_theme_dark_onTertiary = Color(0xFF243600)
|
||||
//val md_theme_dark_tertiaryContainer = Color(0xFF364E00)
|
||||
//val md_theme_dark_onTertiaryContainer = Color(0xFFCBEF86)
|
||||
//val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
//val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
//val md_theme_dark_onError = Color(0xFF690005)
|
||||
//val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
//val md_theme_dark_background = Color(0xFF032100)
|
||||
//val md_theme_dark_onBackground = Color(0xFFB4F39B)
|
||||
//val md_theme_dark_surface = Color(0xFF032100)
|
||||
//val md_theme_dark_onSurface = Color(0xFFB4F39B)
|
||||
//val md_theme_dark_surfaceVariant = Color(0xFF4C4639)
|
||||
//val md_theme_dark_onSurfaceVariant = Color(0xFFCFC5B4)
|
||||
//val md_theme_dark_outline = Color(0xFF989080)
|
||||
//val md_theme_dark_inverseOnSurface = Color(0xFF032100)
|
||||
//val md_theme_dark_inverseSurface = Color(0xFFB4F39B)
|
||||
//val md_theme_dark_inversePrimary = Color(0xFF745B00)
|
||||
//val md_theme_dark_shadow = Color(0xFF000000)
|
||||
//val md_theme_dark_surfaceTint = Color(0xFFEFC125)
|
||||
//
|
||||
//val seed = Color(0xFFE2B513)
|
||||
val md_theme_dark_onPrimary = Color(0xFF212121)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF212121)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
|
||||
val md_theme_dark_secondary = Color(0xFFDDC2A1)
|
||||
val md_theme_dark_onSecondary = Color(0xFF212121)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF283237)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
|
||||
val md_theme_dark_tertiary = Color(0xFFB8CEA1)
|
||||
val md_theme_dark_onTertiary = Color(0xFF243515)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
|
||||
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
val md_theme_dark_onError = Color(0xFF690005)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_dark_background = Color(0xFF262626)
|
||||
val md_theme_dark_onBackground = Color(0xFFEAE1D9)
|
||||
val md_theme_dark_surface = Color(0xFF1F1B16)
|
||||
val md_theme_dark_onSurface = Color(0xFFEAE1D9)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF212121)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
|
||||
val md_theme_dark_outline = Color(0xFF9C8F80)
|
||||
val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
|
||||
val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
|
||||
val md_theme_dark_inversePrimary = Color(0xFF212121)
|
||||
val md_theme_dark_shadow = Color(0xFF000000)
|
||||
val md_theme_dark_surfaceTint = Color(0xFFFFB951)
|
||||
val md_theme_dark_outlineVariant = Color(0xFF424242)
|
||||
val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
|
||||
// val seed = Color(0xFF825500)
|
||||
|
|
|
|||
|
|
@ -2,32 +2,88 @@ package app.omnivore.omnivore.ui.theme
|
|||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private val LightColors = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
|
||||
private val DarkColors = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun OmnivoreTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
useDynamicTheme: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S,
|
||||
content: @Composable () -> Unit
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
useDynamicTheme: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = if (darkTheme) DarkColors else LightColors
|
||||
val colorScheme = if (darkTheme) DarkColors else LightColors
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
// shapes = Shapes,
|
||||
content = content
|
||||
)
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
|
|||
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 -->
|
||||
|
|
@ -139,6 +139,7 @@
|
|||
<string name="highlights_list_error_msg_no_highlights">You have not added any highlights to this page.</string>
|
||||
|
||||
<!-- ReaderPreferencesView -->
|
||||
<string name="reader_preferences_font">Font:</string>
|
||||
<string name="reader_preferences_view_font_size">Font Size:</string>
|
||||
<string name="reader_preferences_view_margin">Margin</string>
|
||||
<string name="reader_preferences_view_line_spacing">Line Spacing</string>
|
||||
|
|
@ -208,6 +209,23 @@
|
|||
<string name="settings_view_setting_row_manage_account">Manage Account</string>
|
||||
<string name="settings_view_setting_row_logout">Logout</string>
|
||||
|
||||
<!-- stock filters -->
|
||||
<string name="library_filter_inbox">Inbox</string>
|
||||
<string name="library_filter_non_feed">Non-Feed Items</string>
|
||||
<string name="library_filter_feeds">Feeds</string>
|
||||
<string name="library_filter_newsletters">Newsletters</string>
|
||||
<string name="library_filter_recommended">Recommended</string>
|
||||
<string name="library_filter_all">All</string>
|
||||
<string name="library_filter_archived">Archived</string>
|
||||
<string name="library_filter_highlighted">Highlighted</string>
|
||||
<string name="library_filter_files">Files</string>
|
||||
|
||||
<!-- stock sorts -->
|
||||
<string name="library_sort_newest">Newest</string>
|
||||
<string name="library_sort_oldest">Oldest</string>
|
||||
<string name="library_sort_recently_read">Recently read</string>
|
||||
<string name="library_sort_recently_published">Recently published</string>
|
||||
|
||||
<!-- AddLinkSheet -->
|
||||
<string name="add_link_sheet_title">Add Link</string>
|
||||
<string name="add_link_sheet_text_field_placeholder">Add Link</string>
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
package app.omnivore.omnivore
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -1388,7 +1388,7 @@
|
|||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.41.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.41.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.41.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.41.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"] }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,10 +164,13 @@ import Views
|
|||
|
||||
var subPredicates = [NSPredicate]()
|
||||
|
||||
let folderPredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(Models.LibraryItem.folder), filterState.folder
|
||||
)
|
||||
subPredicates.append(folderPredicate)
|
||||
// TODO: FOLLOWING MIGRATION: invert this once the following migration has completed
|
||||
if !UserDefaults.standard.bool(forKey: "LibraryTabView::hideFollowingTab") {
|
||||
let folderPredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(Models.LibraryItem.folder), filterState.folder
|
||||
)
|
||||
subPredicates.append(folderPredicate)
|
||||
}
|
||||
|
||||
if let predicate = filterState.appliedFilter?.predicate {
|
||||
subPredicates.append(predicate)
|
||||
|
|
@ -255,7 +258,11 @@ import Views
|
|||
}.joined(separator: ","))
|
||||
}
|
||||
|
||||
query.append(" use:folders")
|
||||
// TODO: FOLLOWING MIGRATION: invert this once the following migration has completed
|
||||
if !UserDefaults.standard.bool(forKey: "LibraryTabView::hideFollowingTab") {
|
||||
query.append(" use:folders")
|
||||
}
|
||||
|
||||
print("QUERY: `\(query)`")
|
||||
|
||||
return query
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ struct FiltersHeader: View {
|
|||
viewModel.searchTerm = ""
|
||||
}.frame(maxWidth: reader.size.width * 0.66)
|
||||
} else {
|
||||
// if UIDevice.isIPhone {
|
||||
let hideFollowingTab = UserDefaults.standard.bool(forKey: "LibraryTabView::hideFollowingTab")
|
||||
Menu(
|
||||
content: {
|
||||
ForEach(viewModel.filters.filter { $0.folder == viewModel.currentFolder }) { filter in
|
||||
ForEach(viewModel.filters.filter { hideFollowingTab || $0.folder == viewModel.currentFolder }) { filter in
|
||||
Button(filter.name, action: {
|
||||
viewModel.appliedFilter = filter
|
||||
})
|
||||
|
|
@ -188,9 +188,10 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
@State var showAddLinkView = false
|
||||
@State var isListScrolled = false
|
||||
@State var listTitle = ""
|
||||
@State var isEditMode: EditMode = .inactive
|
||||
@State var showExpandedAudioPlayer = false
|
||||
|
||||
@Binding var isEditMode: EditMode
|
||||
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
|
|
@ -200,8 +201,9 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
@State private var selection = Set<String>()
|
||||
|
||||
init(viewModel: HomeFeedViewModel) {
|
||||
init(viewModel: HomeFeedViewModel, isEditMode: Binding<EditMode>) {
|
||||
_viewModel = ObservedObject(wrappedValue: viewModel)
|
||||
_isEditMode = isEditMode
|
||||
}
|
||||
|
||||
func loadItems(isRefresh: Bool) {
|
||||
|
|
@ -293,7 +295,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
LibraryAddLinkView()
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
|
||||
.sheet(isPresented: $showExpandedAudioPlayer) {
|
||||
ExpandedAudioPlayer(
|
||||
delete: {
|
||||
showExpandedAudioPlayer = false
|
||||
|
|
@ -328,7 +330,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
viewModel.selectedItem = linkedItem
|
||||
viewModel.linkIsActive = true
|
||||
}
|
||||
.fullScreenCover(isPresented: $searchPresented) {
|
||||
.sheet(isPresented: $searchPresented) {
|
||||
LibrarySearchView(homeFeedViewModel: self.viewModel)
|
||||
}
|
||||
.task {
|
||||
|
|
@ -882,7 +884,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")
|
||||
|
|
@ -891,7 +895,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") },
|
||||
|
|
|
|||
|
|
@ -167,10 +167,9 @@ enum LoadingBarStyle {
|
|||
let availableFolders = folderConfigs.keys
|
||||
let appliedFilterName = UserDefaults.standard.string(forKey: filterKey)
|
||||
|
||||
filters = newFilters
|
||||
filters = (defaultFilters + newFilters)
|
||||
.filter { availableFolders.contains($0.folder) }
|
||||
.sorted(by: { $0.position < $1.position })
|
||||
+ defaultFilters
|
||||
|
||||
if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != appliedFilter?.id {
|
||||
appliedFilter = newFilter
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import SwiftUI
|
||||
import Utils
|
||||
import Views
|
||||
|
||||
@MainActor
|
||||
struct HomeView: View {
|
||||
@State private var viewModel: HomeFeedViewModel
|
||||
|
||||
init(viewModel: HomeFeedViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
#if os(iOS)
|
||||
HomeFeedContainerView(viewModel: viewModel)
|
||||
#elseif os(macOS)
|
||||
HomeFeedView(viewModel: viewModel)
|
||||
.frame(minWidth: 320)
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Button(
|
||||
action: {
|
||||
NSApp.keyWindow?.firstResponder?.tryToPerform(
|
||||
#selector(NSSplitViewController.toggleSidebar(_:)), with: nil
|
||||
)
|
||||
},
|
||||
label: { Label(LocalText.navigationSelectSidebarToggle, systemImage: "sidebar.left") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import SwiftUI
|
|||
@MainActor
|
||||
public struct LibrarySplitView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@State var isEditMode: EditMode = .inactive
|
||||
|
||||
@StateObject private var viewModel = HomeFeedViewModel(
|
||||
filterKey: "lastSelected",
|
||||
|
|
@ -37,7 +38,7 @@ public struct LibrarySplitView: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("")
|
||||
|
||||
HomeFeedContainerView(viewModel: viewModel)
|
||||
HomeFeedContainerView(viewModel: viewModel, isEditMode: $isEditMode)
|
||||
.navigationViewStyle(.stack)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
|
@ -47,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)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ struct LibraryTabView: View {
|
|||
|
||||
@AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false
|
||||
@AppStorage(UserDefaultKey.lastSelectedTabItem.rawValue) var selectedTab = "inbox"
|
||||
|
||||
@State var isEditMode: EditMode = .inactive
|
||||
@State var showExpandedAudioPlayer = false
|
||||
|
||||
private let syncManager = LibrarySyncManager()
|
||||
|
|
@ -74,14 +76,14 @@ struct LibraryTabView: View {
|
|||
TabView(selection: $selectedTab) {
|
||||
if !hideFollowingTab {
|
||||
NavigationView {
|
||||
HomeFeedContainerView(viewModel: followingViewModel)
|
||||
HomeFeedContainerView(viewModel: followingViewModel, isEditMode: $isEditMode)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationViewStyle(.stack)
|
||||
}.tag("following")
|
||||
}
|
||||
|
||||
NavigationView {
|
||||
HomeFeedContainerView(viewModel: inboxViewModel)
|
||||
HomeFeedContainerView(viewModel: inboxViewModel, isEditMode: $isEditMode)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationViewStyle(.stack)
|
||||
}.tag("inbox")
|
||||
|
|
@ -101,10 +103,12 @@ struct LibraryTabView: View {
|
|||
.frame(height: 1)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
CustomTabBar(selectedTab: $selectedTab, hideFollowingTab: hideFollowingTab)
|
||||
.padding(0)
|
||||
if isEditMode != .active {
|
||||
CustomTabBar(selectedTab: $selectedTab, hideFollowingTab: hideFollowingTab)
|
||||
.padding(0)
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
|
||||
.sheet(isPresented: $showExpandedAudioPlayer) {
|
||||
ExpandedAudioPlayer(
|
||||
delete: {
|
||||
showExpandedAudioPlayer = false
|
||||
|
|
@ -131,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):
|
||||
|
|
@ -140,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)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,11 @@ struct FiltersView: View {
|
|||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("ScrollToTop"))) { _ in
|
||||
dismiss()
|
||||
}
|
||||
.onChange(of: viewModel.hideFollowingTab) { _ in
|
||||
UserDefaults.standard.setValue(nil, forKey: "lastSelected")
|
||||
UserDefaults.standard.setValue(nil, forKey: "lastSelectedFilter-inbox")
|
||||
UserDefaults.standard.setValue(nil, forKey: "lastSelectedFilter-following")
|
||||
}
|
||||
}
|
||||
|
||||
private var innerBody: some View {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -71,13 +71,16 @@ extension DataService {
|
|||
)
|
||||
}
|
||||
|
||||
let sort = InputObjects.SortParams(by: .updatedTime,
|
||||
order: OptionalArgument(descending ? .descending : .ascending))
|
||||
let sort = InputObjects.SortParams(
|
||||
by: .updatedTime,
|
||||
order: OptionalArgument(descending ? .descending : .ascending)
|
||||
)
|
||||
|
||||
let query = Selection.Query {
|
||||
try $0.updatesSince(
|
||||
after: OptionalArgument(cursor),
|
||||
first: OptionalArgument(limit),
|
||||
folder: OptionalArgument("all"),
|
||||
since: DateTime(from: since),
|
||||
sort: OptionalArgument(sort),
|
||||
selection: selection
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "",
|
||||
visible: true,
|
||||
position: -1,
|
||||
position: 11,
|
||||
defaultFilter: true
|
||||
)
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "in:trash",
|
||||
visible: true,
|
||||
position: -1,
|
||||
position: 12,
|
||||
defaultFilter: true
|
||||
)
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "in:inbox is:unread",
|
||||
visible: true,
|
||||
position: -1,
|
||||
position: 10,
|
||||
defaultFilter: true
|
||||
)
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "following",
|
||||
filter: "",
|
||||
visible: true,
|
||||
position: -1,
|
||||
position: 11,
|
||||
defaultFilter: true
|
||||
)
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "following",
|
||||
filter: "in:trash",
|
||||
visible: true,
|
||||
position: -1,
|
||||
position: 12,
|
||||
defaultFilter: true
|
||||
)
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "in:inbox is:unread",
|
||||
visible: true,
|
||||
position: -1,
|
||||
position: 10,
|
||||
defaultFilter: true
|
||||
)
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "",
|
||||
visible: true,
|
||||
position: 0,
|
||||
position: 10,
|
||||
defaultFilter: true
|
||||
),
|
||||
InternalFilter(
|
||||
|
|
@ -104,7 +104,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "",
|
||||
visible: true,
|
||||
position: 1,
|
||||
position: 11,
|
||||
defaultFilter: true
|
||||
),
|
||||
InternalFilter(
|
||||
|
|
@ -113,7 +113,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "",
|
||||
visible: true,
|
||||
position: 2,
|
||||
position: 12,
|
||||
defaultFilter: true
|
||||
),
|
||||
InternalFilter(
|
||||
|
|
@ -122,7 +122,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "",
|
||||
visible: true,
|
||||
position: 3,
|
||||
position: 13,
|
||||
defaultFilter: true
|
||||
),
|
||||
InternalFilter(
|
||||
|
|
@ -131,7 +131,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "is:archived",
|
||||
visible: true,
|
||||
position: 4,
|
||||
position: 14,
|
||||
defaultFilter: true
|
||||
),
|
||||
InternalFilter(
|
||||
|
|
@ -140,7 +140,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "type:file",
|
||||
visible: true,
|
||||
position: 5,
|
||||
position: 15,
|
||||
defaultFilter: true
|
||||
),
|
||||
InternalFilter(
|
||||
|
|
@ -149,7 +149,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "has:highlights",
|
||||
visible: true,
|
||||
position: 6,
|
||||
position: 16,
|
||||
defaultFilter: true
|
||||
),
|
||||
InternalFilter(
|
||||
|
|
@ -158,7 +158,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "inbox",
|
||||
filter: "in:all",
|
||||
visible: true,
|
||||
position: 7,
|
||||
position: 17,
|
||||
defaultFilter: true
|
||||
)
|
||||
]
|
||||
|
|
@ -172,7 +172,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "following",
|
||||
filter: "in:following",
|
||||
visible: true,
|
||||
position: 1,
|
||||
position: 10,
|
||||
defaultFilter: true
|
||||
),
|
||||
InternalFilter(
|
||||
|
|
@ -181,7 +181,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "following",
|
||||
filter: "in:following label:RSS",
|
||||
visible: true,
|
||||
position: 2,
|
||||
position: 12,
|
||||
defaultFilter: true
|
||||
),
|
||||
InternalFilter(
|
||||
|
|
@ -190,7 +190,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
folder: "following",
|
||||
filter: "in:following label:Newsletter",
|
||||
visible: true,
|
||||
position: 3,
|
||||
position: 13,
|
||||
defaultFilter: true
|
||||
)
|
||||
]
|
||||
|
|
@ -209,9 +209,6 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
}
|
||||
|
||||
public var predicate: NSPredicate? {
|
||||
let folderPredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(Models.LibraryItem.folder), folder
|
||||
)
|
||||
let undeletedPredicate = NSPredicate(
|
||||
format: "%K != %i AND %K != \"DELETED\"",
|
||||
#keyPath(Models.LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue),
|
||||
|
|
@ -226,16 +223,16 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
let feedLabelPredicate = NSPredicate(
|
||||
format: "SUBQUERY(labels, $label, $label.name == \"RSS\").@count > 0"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, notInArchivePredicate, undeletedPredicate, feedLabelPredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, undeletedPredicate, feedLabelPredicate])
|
||||
case "Following":
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, notInArchivePredicate, undeletedPredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, undeletedPredicate])
|
||||
case "Inbox":
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate, notInArchivePredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate])
|
||||
case "Unread":
|
||||
let isUnread = NSPredicate(
|
||||
format: "readAt == nil"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate, notInArchivePredicate, isUnread])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, isUnread])
|
||||
case "Non-Feed Items":
|
||||
// non-archived or deleted items without the Newsletter label
|
||||
let nonNewsletterLabelPredicate = NSPredicate(
|
||||
|
|
@ -245,7 +242,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
format: "NOT SUBQUERY(labels, $label, $label.name == \"RSS\") .@count > 0"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
folderPredicate, undeletedPredicate, notInArchivePredicate, nonNewsletterLabelPredicate, nonRSSPredicate
|
||||
undeletedPredicate, notInArchivePredicate, nonNewsletterLabelPredicate, nonRSSPredicate
|
||||
])
|
||||
case "Downloaded":
|
||||
// include pdf only
|
||||
|
|
@ -259,51 +256,50 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
|
|||
format: "localPDF.length > 0"
|
||||
)
|
||||
let downloadedPDF = NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, isPDFPredicate, localPDFURL])
|
||||
return NSCompoundPredicate(orPredicateWithSubpredicates: [folderPredicate, hasHTMLContent, downloadedPDF])
|
||||
return NSCompoundPredicate(orPredicateWithSubpredicates: [hasHTMLContent, downloadedPDF])
|
||||
case "Newsletters":
|
||||
// non-archived or deleted items with the Newsletter label
|
||||
let newsletterLabelPredicate = NSPredicate(
|
||||
format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate, notInArchivePredicate, newsletterLabelPredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, newsletterLabelPredicate])
|
||||
case "Feeds":
|
||||
let feedLabelPredicate = NSPredicate(
|
||||
format: "SUBQUERY(labels, $label, $label.name == \"RSS\").@count > 0"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate, notInArchivePredicate, feedLabelPredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, feedLabelPredicate])
|
||||
case "Recommended":
|
||||
// non-archived or deleted items with the Newsletter label
|
||||
let recommendedPredicate = NSPredicate(
|
||||
format: "recommendations.@count > 0"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate, notInArchivePredicate, recommendedPredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, recommendedPredicate])
|
||||
case "All":
|
||||
// include everything undeleted
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate])
|
||||
case "Archived":
|
||||
let inArchivePredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(Models.LibraryItem.isArchived), Int(truncating: true) as NSNumber
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate, inArchivePredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate])
|
||||
case "Deleted":
|
||||
let deletedPredicate = NSPredicate(
|
||||
format: "%K == %i OR %K == \"DELETED\"",
|
||||
#keyPath(Models.LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue),
|
||||
#keyPath(Models.LibraryItem.state)
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, deletedPredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [deletedPredicate])
|
||||
case "Files":
|
||||
// include pdf only
|
||||
let isPDFPredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(Models.LibraryItem.contentReader), "PDF"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate, isPDFPredicate])
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, isPDFPredicate])
|
||||
case "Highlights":
|
||||
let hasHighlightsPredicate = NSPredicate(
|
||||
format: "highlights.@count > 0"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
folderPredicate,
|
||||
undeletedPredicate,
|
||||
hasHighlightsPredicate
|
||||
])
|
||||
|
|
|
|||
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,72 @@
|
|||
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
|
||||
|
||||
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)"
|
||||
}
|
||||
|
||||
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 +83,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 +129,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
|
||||
|
|
|
|||
|
|
@ -123,7 +123,6 @@ export const mergeHighlightResolver = authorized<
|
|||
...newHighlightInput,
|
||||
annotation:
|
||||
mergedAnnotations.length > 0 ? mergedAnnotations.join('\n') : null,
|
||||
labels: mergedLabels,
|
||||
color,
|
||||
user: { id: uid },
|
||||
libraryItem: { id: input.articleId },
|
||||
|
|
@ -134,6 +133,7 @@ export const mergeHighlightResolver = authorized<
|
|||
const newHighlight = await mergeHighlights(
|
||||
overlapHighlightIdList,
|
||||
highlight,
|
||||
mergedLabels,
|
||||
input.articleId,
|
||||
uid,
|
||||
pubsub
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { diff_match_patch } from 'diff-match-patch'
|
||||
import { DeepPartial } from 'typeorm'
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'
|
||||
import { EntityLabel } from '../entity/entity_label'
|
||||
import { Highlight } from '../entity/highlight'
|
||||
import { Label } from '../entity/label'
|
||||
import { homePageURL } from '../env'
|
||||
import { createPubSubClient, EntityType } from '../pubsub'
|
||||
import { authTrx } from '../repository'
|
||||
|
|
@ -65,6 +67,7 @@ export const createHighlight = async (
|
|||
export const mergeHighlights = async (
|
||||
highlightsToRemove: string[],
|
||||
highlightToAdd: DeepPartial<Highlight>,
|
||||
labels: Label[],
|
||||
libraryItemId: string,
|
||||
userId: string,
|
||||
pubsub = createPubSubClient()
|
||||
|
|
@ -75,6 +78,17 @@ export const mergeHighlights = async (
|
|||
await highlightRepo.delete(highlightsToRemove)
|
||||
|
||||
const newHighlight = await highlightRepo.createAndSave(highlightToAdd)
|
||||
|
||||
if (labels.length > 0) {
|
||||
// save new labels
|
||||
await tx.getRepository(EntityLabel).save(
|
||||
labels.map((l) => ({
|
||||
labelId: l.id,
|
||||
highlightId: newHighlight.id,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
return highlightRepo.findOneOrFail({
|
||||
where: { id: newHighlight.id },
|
||||
relations: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import { User } from '../../src/entity/user'
|
|||
import {
|
||||
createHighlight,
|
||||
deleteHighlightById,
|
||||
findHighlightById,
|
||||
} from '../../src/services/highlights'
|
||||
import { createLabel, saveLabelsInHighlight } from '../../src/services/labels'
|
||||
import { deleteUser } from '../../src/services/user'
|
||||
import { createTestLibraryItem, createTestUser } from '../db'
|
||||
import { generateFakeUuid, graphqlRequest, request } from '../util'
|
||||
import { generateFakeShortId, generateFakeUuid, graphqlRequest, request } from '../util'
|
||||
|
||||
chai.use(chaiString)
|
||||
|
||||
|
|
@ -227,14 +229,18 @@ describe('Highlights API', () => {
|
|||
context('mergeHighlightMutation', () => {
|
||||
let highlightId: string
|
||||
|
||||
before(async () => {
|
||||
beforeEach(async () => {
|
||||
// create test highlight
|
||||
highlightId = generateFakeUuid()
|
||||
const shortHighlightId = '_short_id_1'
|
||||
const shortHighlightId = generateFakeShortId()
|
||||
const query = createHighlightQuery(itemId, highlightId, shortHighlightId)
|
||||
await graphqlRequest(query, authToken).expect(200)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteHighlightById(highlightId)
|
||||
})
|
||||
|
||||
it('should not fail', async () => {
|
||||
const newHighlightId = generateFakeUuid()
|
||||
const newShortHighlightId = '_short_id_2'
|
||||
|
|
@ -257,6 +263,35 @@ describe('Highlights API', () => {
|
|||
expect(
|
||||
res.body.data.mergeHighlight.highlight.highlightPositionAnchorIndex
|
||||
).to.eq(highlightPositionAnchorIndex)
|
||||
|
||||
highlightId = newHighlightId
|
||||
})
|
||||
|
||||
it('keeps the labels of the merged highlight', async () => {
|
||||
// create label
|
||||
const labelName = 'test label'
|
||||
const labelColor = '#ff0000'
|
||||
const label = await createLabel(labelName, labelColor, user.id)
|
||||
|
||||
await saveLabelsInHighlight([label], highlightId, user.id)
|
||||
|
||||
const newHighlightId = generateFakeUuid()
|
||||
const newShortHighlightId = generateFakeShortId()
|
||||
const query = mergeHighlightQuery(
|
||||
itemId,
|
||||
newHighlightId,
|
||||
newShortHighlightId,
|
||||
[highlightId],
|
||||
)
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
|
||||
expect(res.body.data.mergeHighlight.highlight.id).to.eq(newHighlightId)
|
||||
|
||||
const highlight = await findHighlightById(newHighlightId, user.id)
|
||||
expect(highlight.labels).to.have.lengthOf(1)
|
||||
expect(highlight.labels?.[0]?.name).to.eq(labelName)
|
||||
|
||||
highlightId = newHighlightId
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { nanoid } from 'nanoid'
|
||||
import supertest from 'supertest'
|
||||
import { v4 } from 'uuid'
|
||||
import { createApp } from '../src/server'
|
||||
|
|
@ -18,7 +19,7 @@ export const stopApolloServer = async () => {
|
|||
export const graphqlRequest = (
|
||||
query: string,
|
||||
authToken: string,
|
||||
variables?: Record<string, unknown>,
|
||||
variables?: Record<string, unknown>
|
||||
): supertest.Test => {
|
||||
return request
|
||||
.post(apollo.graphqlPath)
|
||||
|
|
@ -31,3 +32,7 @@ export const graphqlRequest = (
|
|||
export const generateFakeUuid = () => {
|
||||
return v4()
|
||||
}
|
||||
|
||||
export const generateFakeShortId = () => {
|
||||
return nanoid(8)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
@ -83,8 +83,8 @@ const App = () => {
|
|||
articleMutations={{
|
||||
createHighlightMutation: (input) =>
|
||||
mutation('createHighlight', input),
|
||||
deleteHighlightMutation: (highlightId) =>
|
||||
mutation('deleteHighlight', { highlightId }),
|
||||
deleteHighlightMutation: (libraryItemId, highlightId) =>
|
||||
mutation('deleteHighlight', { libraryItemId, highlightId }),
|
||||
mergeHighlightMutation: (input) =>
|
||||
mutation('mergeHighlight', input),
|
||||
updateHighlightMutation: (input) =>
|
||||
|
|
|
|||
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;
|
||||
|
|
@ -127,15 +127,16 @@ export const exporter = Sentry.GCPFunction.wrapHttpFunction(
|
|||
hasMore = response.data.search.pageInfo.hasNextPage
|
||||
after = response.data.search.pageInfo.endCursor
|
||||
const items = response.data.search.edges.map((edge) => edge.node)
|
||||
if (items.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const size = items.length
|
||||
console.log('exporting items...', {
|
||||
userId: claims.uid,
|
||||
total: items.length,
|
||||
size,
|
||||
hasMore,
|
||||
})
|
||||
|
||||
if (size === 0) {
|
||||
break
|
||||
}
|
||||
const synced = await client.export(claims.token, items)
|
||||
if (!synced) {
|
||||
console.error('failed to export item', {
|
||||
|
|
@ -144,16 +145,17 @@ export const exporter = Sentry.GCPFunction.wrapHttpFunction(
|
|||
return res.status(400).send('Failed to sync')
|
||||
}
|
||||
|
||||
const lastItemUpdatedAt = items[size - 1].updatedAt
|
||||
console.log('updating integration...', {
|
||||
userId: claims.uid,
|
||||
integrationId,
|
||||
syncedAt: items[items.length - 1].updatedAt,
|
||||
syncedAt: lastItemUpdatedAt,
|
||||
})
|
||||
// update integration syncedAt if successful
|
||||
const updated = await updateIntegration(
|
||||
REST_BACKEND_ENDPOINT,
|
||||
integrationId,
|
||||
items[items.length - 1].updatedAt,
|
||||
lastItemUpdatedAt,
|
||||
integrationName,
|
||||
claims.token,
|
||||
systemToken,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ export const search = async (
|
|||
first = 50,
|
||||
after = '0'
|
||||
): Promise<SearchResponse | null> => {
|
||||
const query = `updated:${updatedSince.toISOString()} ${
|
||||
// get all the items updated since the last sync including archived items
|
||||
const query = `in:all updated:${updatedSince.toISOString()} ${
|
||||
highlightOnly ? 'has:highlights' : ''
|
||||
} sort:updated-asc`
|
||||
|
||||
|
|
|
|||
|
|
@ -78,12 +78,15 @@ const isFeedBlocked = async (feedUrl: string, redisClient: RedisClient) => {
|
|||
return false
|
||||
}
|
||||
|
||||
const blockFeed = async (feedUrl: string, redisClient: RedisClient) => {
|
||||
const incrementFeedFailure = async (
|
||||
feedUrl: string,
|
||||
redisClient: RedisClient
|
||||
) => {
|
||||
const key = feedFetchFailedRedisKey(feedUrl)
|
||||
try {
|
||||
const result = await redisClient.incr(key)
|
||||
// expire the key in 1 day
|
||||
await redisClient.expire(key, 24 * 60 * 60, 'NX')
|
||||
await redisClient.expire(key, 24 * 60 * 60)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
|
|
@ -142,8 +145,8 @@ export const fetchAndChecksum = async (url: string) => {
|
|||
|
||||
return { url, content: dataStr, checksum: hash.digest('hex') }
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw new Error(`Failed to fetch or hash content from ${url}.`)
|
||||
console.log(`Failed to fetch or hash content from ${url}.`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +185,9 @@ const parseFeed = async (url: string, content: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
return parser.parseString(content)
|
||||
// return await is needed to catch errors thrown by the parser
|
||||
// otherwise the error will be caught by the outer try catch
|
||||
return await parser.parseString(content)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return null
|
||||
|
|
@ -600,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,
|
||||
|
|
@ -631,10 +635,16 @@ export const rssHandler = Sentry.GCPFunction.wrapHttpFunction(
|
|||
}
|
||||
|
||||
const fetchResult = await fetchAndChecksum(feedUrl)
|
||||
if (!fetchResult) {
|
||||
console.error('Failed to fetch RSS feed', feedUrl)
|
||||
await incrementFeedFailure(feedUrl, redisClient)
|
||||
return res.status(500).send('FAILED_TO_FETCH_RSS_FEED')
|
||||
}
|
||||
|
||||
const feed = await parseFeed(feedUrl, fetchResult.content)
|
||||
if (!feed) {
|
||||
console.error('Failed to parse RSS feed', feedUrl)
|
||||
await blockFeed(feedUrl, redisClient)
|
||||
await incrementFeedFailure(feedUrl, redisClient)
|
||||
return res.status(500).send('INVALID_RSS_FEED')
|
||||
}
|
||||
|
||||
|
|
@ -667,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')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ describe('fetchAndChecksum', () => {
|
|||
it('should hash the content available', async () => {
|
||||
nock('https://fake.com', {}).get('/rss.xml').reply(200, 'i am some content')
|
||||
const result = await fetchAndChecksum('https://fake.com/rss.xml')
|
||||
expect(result.checksum).to.eq(
|
||||
expect(result?.checksum).to.eq(
|
||||
'd6bc10faec048d999d0cf4b2f7103d84557fb9cd94c3bccd17884b1288949375'
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
|
|||
;(async () => {
|
||||
const success = await updateHighlightMutation({
|
||||
annotation: text,
|
||||
libraryItemId: props.targetId,
|
||||
highlightId: props.highlight?.id,
|
||||
})
|
||||
if (success) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
|
|||
;(async () => {
|
||||
const success = await updateHighlightMutation({
|
||||
annotation: text,
|
||||
libraryItemId: props.targetId,
|
||||
highlightId: props.highlight?.id,
|
||||
})
|
||||
if (success) {
|
||||
|
|
|
|||
|
|
@ -310,6 +310,7 @@ export default function EpubContainer(props: EpubContainerProps): JSX.Element {
|
|||
{noteTarget && (
|
||||
<HighlightNoteModal
|
||||
highlight={noteTarget}
|
||||
libraryItemId={props.article.id}
|
||||
author={props.article.author ?? ''}
|
||||
title={props.article.title}
|
||||
onUpdate={(highlight: Highlight) => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ type HighlightNoteModalProps = {
|
|||
author: string
|
||||
title: string
|
||||
highlight?: Highlight
|
||||
libraryItemId: string
|
||||
onUpdate: (updatedHighlight: Highlight) => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
createHighlightForNote?: (note?: string) => Promise<Highlight | undefined>
|
||||
|
|
@ -38,6 +39,7 @@ export function HighlightNoteModal(
|
|||
const saveNoteChanges = useCallback(async () => {
|
||||
if (noteContent != props.highlight?.annotation && props.highlight?.id) {
|
||||
const result = await updateHighlightMutation({
|
||||
libraryItemId: props.libraryItemId,
|
||||
highlightId: props.highlight?.id,
|
||||
annotation: noteContent,
|
||||
color: props.highlight?.color,
|
||||
|
|
|
|||
|
|
@ -187,7 +187,10 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
}
|
||||
|
||||
const didDeleteHighlight =
|
||||
await props.articleMutations.deleteHighlightMutation(highlightId)
|
||||
await props.articleMutations.deleteHighlightMutation(
|
||||
props.articleId,
|
||||
highlightId
|
||||
)
|
||||
|
||||
if (didDeleteHighlight) {
|
||||
removeHighlights(
|
||||
|
|
@ -222,6 +225,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
updateHighlightsCallback(highlight)
|
||||
;(async () => {
|
||||
const update = await props.articleMutations.updateHighlightMutation({
|
||||
libraryItemId: props.articleId,
|
||||
highlightId: highlight.id,
|
||||
color: color,
|
||||
})
|
||||
|
|
@ -705,6 +709,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
const annotation = event.annotation ?? ''
|
||||
|
||||
const result = await props.articleMutations.updateHighlightMutation({
|
||||
libraryItemId: props.articleId,
|
||||
highlightId: focusedHighlight.id,
|
||||
annotation: event.annotation ?? '',
|
||||
})
|
||||
|
|
@ -788,6 +793,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
highlight={highlightModalAction.highlight}
|
||||
author={props.articleAuthor}
|
||||
title={props.articleTitle}
|
||||
libraryItemId={props.articleId}
|
||||
onUpdate={updateHighlightsCallback}
|
||||
onOpenChange={() =>
|
||||
setHighlightModalAction({ highlightModalAction: 'none' })
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
(note: Highlight, text: string, startTime: Date) => {
|
||||
;(async () => {
|
||||
const result = await updateHighlightMutation({
|
||||
libraryItemId: props.item.id,
|
||||
highlightId: note.id,
|
||||
annotation: text,
|
||||
})
|
||||
|
|
@ -195,7 +196,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
highlights
|
||||
?.filter((h) => h.type === 'NOTE')
|
||||
.forEach(async (h) => {
|
||||
const result = await deleteHighlightMutation(h.id)
|
||||
const result = await deleteHighlightMutation(props.item.id, h.id)
|
||||
if (!result) {
|
||||
showErrorToast('Error deleting note')
|
||||
}
|
||||
|
|
@ -325,6 +326,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
;(async () => {
|
||||
const highlightId = showConfirmDeleteHighlightId
|
||||
const success = await deleteHighlightMutation(
|
||||
props.item.id,
|
||||
showConfirmDeleteHighlightId
|
||||
)
|
||||
mutate()
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export default function PdfArticleContainer(
|
|||
.delete(annotation)
|
||||
.then(() => {
|
||||
if (annotationId) {
|
||||
return deleteHighlightMutation(annotationId)
|
||||
return deleteHighlightMutation(props.article.id, annotationId)
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
|
|
@ -229,7 +229,7 @@ export default function PdfArticleContainer(
|
|||
}
|
||||
const annotationId = annotationOmnivoreId(annotation)
|
||||
if (annotationId) {
|
||||
await deleteHighlightMutation(annotationId)
|
||||
await deleteHighlightMutation(props.article.id, annotationId)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -512,7 +512,7 @@ export default function PdfArticleContainer(
|
|||
const storedId = annotationOmnivoreId(annotation)
|
||||
if (storedId == annotationId) {
|
||||
await instance.delete(annotation)
|
||||
await deleteHighlightMutation(annotationId)
|
||||
await deleteHighlightMutation(props.article.id, annotationId)
|
||||
|
||||
const highlightIdx = highlightsRef.current.findIndex((value) => {
|
||||
return value.id == annotationId
|
||||
|
|
@ -576,6 +576,7 @@ export default function PdfArticleContainer(
|
|||
{noteTarget && (
|
||||
<HighlightNoteModal
|
||||
highlight={noteTarget}
|
||||
libraryItemId={props.article.id}
|
||||
author={props.article.author ?? ''}
|
||||
title={props.article.title}
|
||||
onUpdate={(highlight: Highlight) => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ export type ArticleMutations = {
|
|||
createHighlightMutation: (
|
||||
input: CreateHighlightInput
|
||||
) => Promise<Highlight | undefined>
|
||||
deleteHighlightMutation: (highlightId: string) => Promise<boolean>
|
||||
deleteHighlightMutation: (
|
||||
libraryItemId: string,
|
||||
highlightId: string
|
||||
) => Promise<boolean>
|
||||
mergeHighlightMutation: (
|
||||
input: MergeHighlightInput
|
||||
) => Promise<Highlight | undefined>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { gql } from 'graphql-request'
|
|||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
export async function deleteHighlightMutation(
|
||||
libraryItemId: string,
|
||||
highlightId: string
|
||||
): Promise<boolean> {
|
||||
const mutation = gql`
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { gqlFetcher } from '../networkHelpers'
|
|||
|
||||
export type UpdateHighlightInput = {
|
||||
highlightId: string
|
||||
libraryItemId?: string
|
||||
annotation?: string
|
||||
sharedAt?: string
|
||||
color?: string
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue