mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #2076 from omnivore-app/fix/android-ui-tweaks
Android tweaks: Set email and password keyboard types on login form, standalone SearchView
This commit is contained in:
commit
fb4ed29e04
37 changed files with 1979 additions and 399 deletions
|
|
@ -17,8 +17,8 @@ android {
|
|||
applicationId "app.omnivore.omnivore"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 33
|
||||
versionName "0.0.33"
|
||||
versionCode 44
|
||||
versionName "0.0.44"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ fragment ArticleFields on Article {
|
|||
readAt
|
||||
updatedAt
|
||||
content
|
||||
wordsCount
|
||||
}
|
||||
|
||||
fragment HighlightFields on Highlight {
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ query Search($after: String, $first: Int, $query: String) {
|
|||
originalArticleUrl
|
||||
uploadFileId
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
...LabelFields
|
||||
}
|
||||
highlights {
|
||||
...HighlightFields
|
||||
}
|
||||
pageId
|
||||
shortId
|
||||
|
|
@ -36,6 +37,7 @@ query Search($after: String, $first: Int, $query: String) {
|
|||
readAt
|
||||
savedAt
|
||||
updatedAt
|
||||
wordsCount
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
|
|
|
|||
|
|
@ -24,9 +24,10 @@ query UpdatesSince($after: String, $first: Int, $since: Date!) {
|
|||
originalArticleUrl
|
||||
uploadFileId
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
...LabelFields
|
||||
}
|
||||
highlights {
|
||||
...HighlightFields
|
||||
}
|
||||
pageId
|
||||
shortId
|
||||
|
|
@ -39,6 +40,7 @@ query UpdatesSince($after: String, $first: Int, $since: Date!) {
|
|||
savedAt
|
||||
updatedAt
|
||||
language
|
||||
wordsCount
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
directive @sanitize(allowedTags: [String], maxLength: Int, pattern: String) on INPUT_FIELD_DEFINITION
|
||||
directive @sanitize(allowedTags: [String], maxLength: Int, minLength: Int, pattern: String) on INPUT_FIELD_DEFINITION
|
||||
|
||||
type AddPopularReadError {
|
||||
errorCodes: [AddPopularReadErrorCode!]!
|
||||
|
|
@ -86,6 +86,8 @@ type Article {
|
|||
readAt: Date
|
||||
readingProgressAnchorIndex: Int!
|
||||
readingProgressPercent: Float!
|
||||
readingProgressTopPercent: Float
|
||||
recommendations: [Recommendation!]
|
||||
savedAt: Date!
|
||||
savedByViewer: Boolean
|
||||
shareInfo: LinkShareInfo
|
||||
|
|
@ -101,6 +103,7 @@ type Article {
|
|||
updatedAt: Date!
|
||||
uploadFileId: ID
|
||||
url: String!
|
||||
wordsCount: Int
|
||||
}
|
||||
|
||||
type ArticleEdge {
|
||||
|
|
@ -132,6 +135,7 @@ type ArticleSavingRequest {
|
|||
slug: String!
|
||||
status: ArticleSavingRequestStatus!
|
||||
updatedAt: Date!
|
||||
url: String!
|
||||
user: User!
|
||||
userId: ID! @deprecated(reason: "userId has been replaced with user")
|
||||
}
|
||||
|
|
@ -141,6 +145,7 @@ type ArticleSavingRequestError {
|
|||
}
|
||||
|
||||
enum ArticleSavingRequestErrorCode {
|
||||
BAD_DATA
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
|
@ -148,6 +153,7 @@ enum ArticleSavingRequestErrorCode {
|
|||
union ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequestSuccess
|
||||
|
||||
enum ArticleSavingRequestStatus {
|
||||
ARCHIVED
|
||||
DELETED
|
||||
FAILED
|
||||
PROCESSING
|
||||
|
|
@ -177,7 +183,27 @@ type ArticlesSuccess {
|
|||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type BulkActionError {
|
||||
errorCodes: [BulkActionErrorCode!]!
|
||||
}
|
||||
|
||||
enum BulkActionErrorCode {
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union BulkActionResult = BulkActionError | BulkActionSuccess
|
||||
|
||||
type BulkActionSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
enum BulkActionType {
|
||||
ARCHIVE
|
||||
DELETE
|
||||
}
|
||||
|
||||
enum ContentReader {
|
||||
EPUB
|
||||
PDF
|
||||
WEB
|
||||
}
|
||||
|
|
@ -198,9 +224,11 @@ enum CreateArticleErrorCode {
|
|||
|
||||
input CreateArticleInput {
|
||||
articleSavingRequestId: ID
|
||||
labels: [CreateLabelInput!]
|
||||
preparedDocument: PreparedDocumentInput
|
||||
skipParsing: Boolean
|
||||
source: String
|
||||
state: ArticleSavingRequestStatus
|
||||
uploadFileId: ID
|
||||
url: String!
|
||||
}
|
||||
|
|
@ -232,6 +260,31 @@ type CreateArticleSuccess {
|
|||
user: User!
|
||||
}
|
||||
|
||||
type CreateGroupError {
|
||||
errorCodes: [CreateGroupErrorCode!]!
|
||||
}
|
||||
|
||||
enum CreateGroupErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input CreateGroupInput {
|
||||
description: String
|
||||
expiresInDays: Int
|
||||
maxMembers: Int
|
||||
name: String!
|
||||
onlyAdminCanPost: Boolean
|
||||
onlyAdminCanSeeMembers: Boolean
|
||||
topics: [String!]
|
||||
}
|
||||
|
||||
union CreateGroupResult = CreateGroupError | CreateGroupSuccess
|
||||
|
||||
type CreateGroupSuccess {
|
||||
group: RecommendationGroup!
|
||||
}
|
||||
|
||||
type CreateHighlightError {
|
||||
errorCodes: [CreateHighlightErrorCode!]!
|
||||
}
|
||||
|
|
@ -247,13 +300,17 @@ enum CreateHighlightErrorCode {
|
|||
input CreateHighlightInput {
|
||||
annotation: String
|
||||
articleId: ID!
|
||||
highlightPositionAnchorIndex: Int
|
||||
highlightPositionPercent: Float
|
||||
html: String
|
||||
id: ID!
|
||||
patch: String!
|
||||
patch: String
|
||||
prefix: String
|
||||
quote: String!
|
||||
quote: String
|
||||
sharedAt: Date
|
||||
shortId: String!
|
||||
suffix: String
|
||||
type: HighlightType
|
||||
}
|
||||
|
||||
type CreateHighlightReplyError {
|
||||
|
|
@ -296,7 +353,7 @@ enum CreateLabelErrorCode {
|
|||
}
|
||||
|
||||
input CreateLabelInput {
|
||||
color: String!
|
||||
color: String
|
||||
description: String
|
||||
name: String!
|
||||
}
|
||||
|
|
@ -388,6 +445,22 @@ type DeleteAccountSuccess {
|
|||
userID: ID!
|
||||
}
|
||||
|
||||
type DeleteFilterError {
|
||||
errorCodes: [DeleteFilterErrorCode!]!
|
||||
}
|
||||
|
||||
enum DeleteFilterErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union DeleteFilterResult = DeleteFilterError | DeleteFilterSuccess
|
||||
|
||||
type DeleteFilterSuccess {
|
||||
filter: Filter!
|
||||
}
|
||||
|
||||
type DeleteHighlightError {
|
||||
errorCodes: [DeleteHighlightErrorCode!]!
|
||||
}
|
||||
|
|
@ -500,6 +573,22 @@ type DeleteReminderSuccess {
|
|||
reminder: Reminder!
|
||||
}
|
||||
|
||||
type DeleteRuleError {
|
||||
errorCodes: [DeleteRuleErrorCode!]!
|
||||
}
|
||||
|
||||
enum DeleteRuleErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union DeleteRuleResult = DeleteRuleError | DeleteRuleSuccess
|
||||
|
||||
type DeleteRuleSuccess {
|
||||
rule: Rule!
|
||||
}
|
||||
|
||||
type DeleteWebhookError {
|
||||
errorCodes: [DeleteWebhookErrorCode!]!
|
||||
}
|
||||
|
|
@ -522,6 +611,31 @@ type DeviceToken {
|
|||
token: String!
|
||||
}
|
||||
|
||||
type DeviceTokensError {
|
||||
errorCodes: [DeviceTokensErrorCode!]!
|
||||
}
|
||||
|
||||
enum DeviceTokensErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union DeviceTokensResult = DeviceTokensError | DeviceTokensSuccess
|
||||
|
||||
type DeviceTokensSuccess {
|
||||
deviceTokens: [DeviceToken!]!
|
||||
}
|
||||
|
||||
type Feature {
|
||||
createdAt: Date!
|
||||
expiresAt: Date
|
||||
grantedAt: Date
|
||||
id: ID!
|
||||
name: String!
|
||||
token: String!
|
||||
updatedAt: Date!
|
||||
}
|
||||
|
||||
type FeedArticle {
|
||||
annotationsCount: Int
|
||||
article: Article!
|
||||
|
|
@ -555,6 +669,31 @@ type FeedArticlesSuccess {
|
|||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type Filter {
|
||||
createdAt: Date!
|
||||
description: String
|
||||
filter: String!
|
||||
id: ID!
|
||||
name: String!
|
||||
position: Int!
|
||||
updatedAt: Date!
|
||||
}
|
||||
|
||||
type FiltersError {
|
||||
errorCodes: [FiltersErrorCode!]!
|
||||
}
|
||||
|
||||
enum FiltersErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union FiltersResult = FiltersError | FiltersSuccess
|
||||
|
||||
type FiltersSuccess {
|
||||
filters: [Filter!]!
|
||||
}
|
||||
|
||||
type GenerateApiKeyError {
|
||||
errorCodes: [GenerateApiKeyErrorCode!]!
|
||||
}
|
||||
|
|
@ -644,19 +783,39 @@ type GoogleSignupSuccess {
|
|||
me: User!
|
||||
}
|
||||
|
||||
type GroupsError {
|
||||
errorCodes: [GroupsErrorCode!]!
|
||||
}
|
||||
|
||||
enum GroupsErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union GroupsResult = GroupsError | GroupsSuccess
|
||||
|
||||
type GroupsSuccess {
|
||||
groups: [RecommendationGroup!]!
|
||||
}
|
||||
|
||||
type Highlight {
|
||||
annotation: String
|
||||
createdAt: Date!
|
||||
createdByMe: Boolean!
|
||||
highlightPositionAnchorIndex: Int
|
||||
highlightPositionPercent: Float
|
||||
html: String
|
||||
id: ID!
|
||||
patch: String!
|
||||
labels: [Label!]
|
||||
patch: String
|
||||
prefix: String
|
||||
quote: String!
|
||||
quote: String
|
||||
reactions: [Reaction!]!
|
||||
replies: [HighlightReply!]!
|
||||
sharedAt: Date
|
||||
shortId: String!
|
||||
suffix: String
|
||||
type: HighlightType!
|
||||
updatedAt: Date!
|
||||
user: User!
|
||||
}
|
||||
|
|
@ -674,17 +833,40 @@ type HighlightStats {
|
|||
highlightCount: Int!
|
||||
}
|
||||
|
||||
enum HighlightType {
|
||||
HIGHLIGHT
|
||||
NOTE
|
||||
REDACTION
|
||||
}
|
||||
|
||||
type ImportFromIntegrationError {
|
||||
errorCodes: [ImportFromIntegrationErrorCode!]!
|
||||
}
|
||||
|
||||
enum ImportFromIntegrationErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union ImportFromIntegrationResult = ImportFromIntegrationError | ImportFromIntegrationSuccess
|
||||
|
||||
type ImportFromIntegrationSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Integration {
|
||||
createdAt: Date!
|
||||
enabled: Boolean!
|
||||
id: ID!
|
||||
name: String!
|
||||
token: String!
|
||||
type: IntegrationType!
|
||||
updatedAt: Date!
|
||||
}
|
||||
|
||||
enum IntegrationType {
|
||||
READWISE
|
||||
EXPORT
|
||||
IMPORT
|
||||
}
|
||||
|
||||
type IntegrationsError {
|
||||
|
|
@ -702,6 +884,22 @@ type IntegrationsSuccess {
|
|||
integrations: [Integration!]!
|
||||
}
|
||||
|
||||
type JoinGroupError {
|
||||
errorCodes: [JoinGroupErrorCode!]!
|
||||
}
|
||||
|
||||
enum JoinGroupErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union JoinGroupResult = JoinGroupError | JoinGroupSuccess
|
||||
|
||||
type JoinGroupSuccess {
|
||||
group: RecommendationGroup!
|
||||
}
|
||||
|
||||
type Label {
|
||||
color: String!
|
||||
createdAt: Date
|
||||
|
|
@ -727,6 +925,22 @@ type LabelsSuccess {
|
|||
labels: [Label!]!
|
||||
}
|
||||
|
||||
type LeaveGroupError {
|
||||
errorCodes: [LeaveGroupErrorCode!]!
|
||||
}
|
||||
|
||||
enum LeaveGroupErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union LeaveGroupResult = LeaveGroupError | LeaveGroupSuccess
|
||||
|
||||
type LeaveGroupSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Link {
|
||||
highlightStats: HighlightStats!
|
||||
id: ID!
|
||||
|
|
@ -782,6 +996,22 @@ type LoginSuccess {
|
|||
me: User!
|
||||
}
|
||||
|
||||
type MarkEmailAsItemError {
|
||||
errorCodes: [MarkEmailAsItemErrorCode!]!
|
||||
}
|
||||
|
||||
enum MarkEmailAsItemErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union MarkEmailAsItemResult = MarkEmailAsItemError | MarkEmailAsItemSuccess
|
||||
|
||||
type MarkEmailAsItemSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type MergeHighlightError {
|
||||
errorCodes: [MergeHighlightErrorCode!]!
|
||||
}
|
||||
|
|
@ -797,6 +1027,9 @@ enum MergeHighlightErrorCode {
|
|||
input MergeHighlightInput {
|
||||
annotation: String
|
||||
articleId: ID!
|
||||
highlightPositionAnchorIndex: Int
|
||||
highlightPositionPercent: Float
|
||||
html: String
|
||||
id: ID!
|
||||
overlapHighlightIdList: [String!]!
|
||||
patch: String!
|
||||
|
|
@ -813,6 +1046,27 @@ type MergeHighlightSuccess {
|
|||
overlapHighlightIdList: [String!]!
|
||||
}
|
||||
|
||||
type MoveFilterError {
|
||||
errorCodes: [MoveFilterErrorCode!]!
|
||||
}
|
||||
|
||||
enum MoveFilterErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input MoveFilterInput {
|
||||
afterFilterId: ID
|
||||
filterId: ID!
|
||||
}
|
||||
|
||||
union MoveFilterResult = MoveFilterError | MoveFilterSuccess
|
||||
|
||||
type MoveFilterSuccess {
|
||||
filter: Filter!
|
||||
}
|
||||
|
||||
type MoveLabelError {
|
||||
errorCodes: [MoveLabelErrorCode!]!
|
||||
}
|
||||
|
|
@ -836,8 +1090,10 @@ type MoveLabelSuccess {
|
|||
|
||||
type Mutation {
|
||||
addPopularRead(name: String!): AddPopularReadResult!
|
||||
bulkAction(action: BulkActionType!, query: String): BulkActionResult!
|
||||
createArticle(input: CreateArticleInput!): CreateArticleResult!
|
||||
createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult!
|
||||
createGroup(input: CreateGroupInput!): CreateGroupResult!
|
||||
createHighlight(input: CreateHighlightInput!): CreateHighlightResult!
|
||||
createHighlightReply(input: CreateHighlightReplyInput!): CreateHighlightReplyResult!
|
||||
createLabel(input: CreateLabelInput!): CreateLabelResult!
|
||||
|
|
@ -845,6 +1101,7 @@ type Mutation {
|
|||
createReaction(input: CreateReactionInput!): CreateReactionResult!
|
||||
createReminder(input: CreateReminderInput!): CreateReminderResult!
|
||||
deleteAccount(userID: ID!): DeleteAccountResult!
|
||||
deleteFilter(id: ID!): DeleteFilterResult!
|
||||
deleteHighlight(highlightId: ID!): DeleteHighlightResult!
|
||||
deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult!
|
||||
deleteIntegration(id: ID!): DeleteIntegrationResult!
|
||||
|
|
@ -852,17 +1109,27 @@ type Mutation {
|
|||
deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
|
||||
deleteReaction(id: ID!): DeleteReactionResult!
|
||||
deleteReminder(id: ID!): DeleteReminderResult!
|
||||
deleteRule(id: ID!): DeleteRuleResult!
|
||||
deleteWebhook(id: ID!): DeleteWebhookResult!
|
||||
generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult!
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
googleSignup(input: GoogleSignupInput!): GoogleSignupResult!
|
||||
importFromIntegration(integrationId: ID!): ImportFromIntegrationResult!
|
||||
joinGroup(inviteCode: String!): JoinGroupResult!
|
||||
leaveGroup(groupId: ID!): LeaveGroupResult!
|
||||
logOut: LogOutResult!
|
||||
markEmailAsItem(recentEmailId: ID!): MarkEmailAsItemResult!
|
||||
mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult!
|
||||
moveFilter(input: MoveFilterInput!): MoveFilterResult!
|
||||
moveLabel(input: MoveLabelInput!): MoveLabelResult!
|
||||
optInFeature(input: OptInFeatureInput!): OptInFeatureResult!
|
||||
recommend(input: RecommendInput!): RecommendResult!
|
||||
recommendHighlights(input: RecommendHighlightsInput!): RecommendHighlightsResult!
|
||||
reportItem(input: ReportItemInput!): ReportItemResult!
|
||||
revokeApiKey(id: ID!): RevokeApiKeyResult!
|
||||
saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult!
|
||||
saveFile(input: SaveFileInput!): SaveResult!
|
||||
saveFilter(input: SaveFilterInput!): SaveFilterResult!
|
||||
savePage(input: SavePageInput!): SaveResult!
|
||||
saveUrl(input: SaveUrlInput!): SaveResult!
|
||||
setBookmarkArticle(input: SetBookmarkArticleInput!): SetBookmarkArticleResult!
|
||||
|
|
@ -872,6 +1139,7 @@ type Mutation {
|
|||
setLabels(input: SetLabelsInput!): SetLabelsResult!
|
||||
setLabelsForHighlight(input: SetLabelsForHighlightInput!): SetLabelsResult!
|
||||
setLinkArchived(input: ArchiveLinkInput!): ArchiveLinkResult!
|
||||
setRule(input: SetRuleInput!): SetRuleResult!
|
||||
setShareArticle(input: SetShareArticleInput!): SetShareArticleResult!
|
||||
setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult!
|
||||
setUserPersonalization(input: SetUserPersonalizationInput!): SetUserPersonalizationResult!
|
||||
|
|
@ -888,12 +1156,15 @@ type Mutation {
|
|||
updateUser(input: UpdateUserInput!): UpdateUserResult!
|
||||
updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResult!
|
||||
uploadFileRequest(input: UploadFileRequestInput!): UploadFileRequestResult!
|
||||
uploadImportFile(contentType: String!, type: UploadImportFileType!): UploadImportFileResult!
|
||||
}
|
||||
|
||||
type NewsletterEmail {
|
||||
address: String!
|
||||
confirmationCode: String
|
||||
createdAt: Date!
|
||||
id: ID!
|
||||
subscriptionCount: Int!
|
||||
}
|
||||
|
||||
type NewsletterEmailsError {
|
||||
|
|
@ -911,6 +1182,25 @@ type NewsletterEmailsSuccess {
|
|||
newsletterEmails: [NewsletterEmail!]!
|
||||
}
|
||||
|
||||
type OptInFeatureError {
|
||||
errorCodes: [OptInFeatureErrorCode!]!
|
||||
}
|
||||
|
||||
enum OptInFeatureErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
input OptInFeatureInput {
|
||||
name: String!
|
||||
}
|
||||
|
||||
union OptInFeatureResult = OptInFeatureError | OptInFeatureSuccess
|
||||
|
||||
type OptInFeatureSuccess {
|
||||
feature: Feature!
|
||||
}
|
||||
|
||||
type Page {
|
||||
author: String
|
||||
createdAt: Date!
|
||||
|
|
@ -951,11 +1241,29 @@ enum PageType {
|
|||
BOOK
|
||||
FILE
|
||||
HIGHLIGHTS
|
||||
IMAGE
|
||||
PROFILE
|
||||
TWEET
|
||||
UNKNOWN
|
||||
VIDEO
|
||||
WEBSITE
|
||||
}
|
||||
|
||||
input ParseResult {
|
||||
byline: String
|
||||
content: String!
|
||||
dir: String
|
||||
excerpt: String!
|
||||
language: String
|
||||
length: Int!
|
||||
previewImage: String
|
||||
publishedDate: Date
|
||||
siteIcon: String
|
||||
siteName: String
|
||||
textContent: String!
|
||||
title: String!
|
||||
}
|
||||
|
||||
input PreparedDocumentInput {
|
||||
document: String!
|
||||
pageInfo: PageInfoInput!
|
||||
|
|
@ -971,25 +1279,31 @@ type Profile {
|
|||
|
||||
type Query {
|
||||
apiKeys: ApiKeysResult!
|
||||
article(slug: String!, username: String!): ArticleResult!
|
||||
articleSavingRequest(id: ID!): ArticleSavingRequestResult!
|
||||
article(format: String, slug: String!, username: String!): ArticleResult!
|
||||
articleSavingRequest(id: ID, url: String): ArticleSavingRequestResult!
|
||||
articles(after: String, first: Int, includePending: Boolean, query: String, sharedOnly: Boolean, sort: SortParams): ArticlesResult!
|
||||
deviceTokens: DeviceTokensResult!
|
||||
feedArticles(after: String, first: Int, sharedByUser: ID, sort: SortParams): FeedArticlesResult!
|
||||
filters: FiltersResult!
|
||||
getFollowers(userId: ID): GetFollowersResult!
|
||||
getFollowing(userId: ID): GetFollowingResult!
|
||||
getUserPersonalization: GetUserPersonalizationResult!
|
||||
groups: GroupsResult!
|
||||
hello: String
|
||||
integrations: IntegrationsResult!
|
||||
labels: LabelsResult!
|
||||
me: User
|
||||
newsletterEmails: NewsletterEmailsResult!
|
||||
recentEmails: RecentEmailsResult!
|
||||
recentSearches: RecentSearchesResult!
|
||||
reminder(linkId: ID!): ReminderResult!
|
||||
search(after: String, first: Int, query: String): SearchResult!
|
||||
rules(enabled: Boolean): RulesResult!
|
||||
search(after: String, first: Int, format: String, includeContent: Boolean, query: String): SearchResult!
|
||||
sendInstallInstructions: SendInstallInstructionsResult!
|
||||
sharedArticle(selectedHighlightId: String, slug: String!, username: String!): SharedArticleResult!
|
||||
subscriptions(sort: SortParams): SubscriptionsResult!
|
||||
typeaheadSearch(first: Int, query: String!): TypeaheadSearchResult!
|
||||
updatesSince(after: String, first: Int, since: Date!): UpdatesSinceResult!
|
||||
updatesSince(after: String, first: Int, since: Date!, sort: SortParams): UpdatesSinceResult!
|
||||
user(userId: ID, username: String): UserResult!
|
||||
users: UsersResult!
|
||||
validateUsername(username: String!): Boolean!
|
||||
|
|
@ -1021,6 +1335,128 @@ type ReadState {
|
|||
readingTime: Int
|
||||
}
|
||||
|
||||
type RecentEmail {
|
||||
createdAt: Date!
|
||||
from: String!
|
||||
html: String
|
||||
id: ID!
|
||||
subject: String!
|
||||
text: String!
|
||||
to: String!
|
||||
type: String!
|
||||
}
|
||||
|
||||
type RecentEmailsError {
|
||||
errorCodes: [RecentEmailsErrorCode!]!
|
||||
}
|
||||
|
||||
enum RecentEmailsErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union RecentEmailsResult = RecentEmailsError | RecentEmailsSuccess
|
||||
|
||||
type RecentEmailsSuccess {
|
||||
recentEmails: [RecentEmail!]!
|
||||
}
|
||||
|
||||
type RecentSearch {
|
||||
createdAt: Date!
|
||||
id: ID!
|
||||
term: String!
|
||||
}
|
||||
|
||||
type RecentSearchesError {
|
||||
errorCodes: [RecentSearchesErrorCode!]!
|
||||
}
|
||||
|
||||
enum RecentSearchesErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union RecentSearchesResult = RecentSearchesError | RecentSearchesSuccess
|
||||
|
||||
type RecentSearchesSuccess {
|
||||
searches: [RecentSearch!]!
|
||||
}
|
||||
|
||||
type RecommendError {
|
||||
errorCodes: [RecommendErrorCode!]!
|
||||
}
|
||||
|
||||
enum RecommendErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
type RecommendHighlightsError {
|
||||
errorCodes: [RecommendHighlightsErrorCode!]!
|
||||
}
|
||||
|
||||
enum RecommendHighlightsErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input RecommendHighlightsInput {
|
||||
groupIds: [ID!]!
|
||||
highlightIds: [ID!]!
|
||||
note: String
|
||||
pageId: ID!
|
||||
}
|
||||
|
||||
union RecommendHighlightsResult = RecommendHighlightsError | RecommendHighlightsSuccess
|
||||
|
||||
type RecommendHighlightsSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
input RecommendInput {
|
||||
groupIds: [ID!]!
|
||||
note: String
|
||||
pageId: ID!
|
||||
recommendedWithHighlights: Boolean
|
||||
}
|
||||
|
||||
union RecommendResult = RecommendError | RecommendSuccess
|
||||
|
||||
type RecommendSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Recommendation {
|
||||
id: ID!
|
||||
name: String!
|
||||
note: String
|
||||
recommendedAt: Date!
|
||||
user: RecommendingUser
|
||||
}
|
||||
|
||||
type RecommendationGroup {
|
||||
admins: [User!]!
|
||||
canPost: Boolean!
|
||||
canSeeMembers: Boolean!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
id: ID!
|
||||
inviteUrl: String!
|
||||
members: [User!]!
|
||||
name: String!
|
||||
topics: [String!]
|
||||
updatedAt: Date!
|
||||
}
|
||||
|
||||
type RecommendingUser {
|
||||
name: String!
|
||||
profileImageURL: String
|
||||
userId: String!
|
||||
username: String!
|
||||
}
|
||||
|
||||
type Reminder {
|
||||
archiveUntil: Boolean!
|
||||
id: ID!
|
||||
|
|
@ -1079,6 +1515,48 @@ type RevokeApiKeySuccess {
|
|||
apiKey: ApiKey!
|
||||
}
|
||||
|
||||
type Rule {
|
||||
actions: [RuleAction!]!
|
||||
createdAt: Date!
|
||||
enabled: Boolean!
|
||||
filter: String!
|
||||
id: ID!
|
||||
name: String!
|
||||
updatedAt: Date!
|
||||
}
|
||||
|
||||
type RuleAction {
|
||||
params: [String!]!
|
||||
type: RuleActionType!
|
||||
}
|
||||
|
||||
input RuleActionInput {
|
||||
params: [String!]!
|
||||
type: RuleActionType!
|
||||
}
|
||||
|
||||
enum RuleActionType {
|
||||
ADD_LABEL
|
||||
ARCHIVE
|
||||
MARK_AS_READ
|
||||
SEND_NOTIFICATION
|
||||
}
|
||||
|
||||
type RulesError {
|
||||
errorCodes: [RulesErrorCode!]!
|
||||
}
|
||||
|
||||
enum RulesErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union RulesResult = RulesError | RulesSuccess
|
||||
|
||||
type RulesSuccess {
|
||||
rules: [Rule!]!
|
||||
}
|
||||
|
||||
type SaveArticleReadingProgressError {
|
||||
errorCodes: [SaveArticleReadingProgressErrorCode!]!
|
||||
}
|
||||
|
|
@ -1093,6 +1571,7 @@ input SaveArticleReadingProgressInput {
|
|||
id: ID!
|
||||
readingProgressAnchorIndex: Int!
|
||||
readingProgressPercent: Float!
|
||||
readingProgressTopPercent: Float
|
||||
}
|
||||
|
||||
union SaveArticleReadingProgressResult = SaveArticleReadingProgressError | SaveArticleReadingProgressSuccess
|
||||
|
|
@ -1107,21 +1586,50 @@ type SaveError {
|
|||
}
|
||||
|
||||
enum SaveErrorCode {
|
||||
EMBEDDED_HIGHLIGHT_FAILED
|
||||
UNAUTHORIZED
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
input SaveFileInput {
|
||||
clientRequestId: ID!
|
||||
labels: [CreateLabelInput!]
|
||||
source: String!
|
||||
state: ArticleSavingRequestStatus
|
||||
uploadFileId: ID!
|
||||
url: String!
|
||||
}
|
||||
|
||||
type SaveFilterError {
|
||||
errorCodes: [SaveFilterErrorCode!]!
|
||||
}
|
||||
|
||||
enum SaveFilterErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input SaveFilterInput {
|
||||
description: String
|
||||
filter: String!
|
||||
id: ID
|
||||
name: String!
|
||||
}
|
||||
|
||||
union SaveFilterResult = SaveFilterError | SaveFilterSuccess
|
||||
|
||||
type SaveFilterSuccess {
|
||||
filter: Filter!
|
||||
}
|
||||
|
||||
input SavePageInput {
|
||||
clientRequestId: ID!
|
||||
labels: [CreateLabelInput!]
|
||||
originalContent: String!
|
||||
parseResult: ParseResult
|
||||
source: String!
|
||||
state: ArticleSavingRequestStatus
|
||||
title: String
|
||||
url: String!
|
||||
}
|
||||
|
|
@ -1135,7 +1643,9 @@ type SaveSuccess {
|
|||
|
||||
input SaveUrlInput {
|
||||
clientRequestId: ID!
|
||||
labels: [CreateLabelInput!]
|
||||
source: String!
|
||||
state: ArticleSavingRequestStatus
|
||||
url: String!
|
||||
}
|
||||
|
||||
|
|
@ -1144,12 +1654,14 @@ type SearchError {
|
|||
}
|
||||
|
||||
enum SearchErrorCode {
|
||||
QUERY_TOO_LONG
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
type SearchItem {
|
||||
annotation: String
|
||||
author: String
|
||||
content: String
|
||||
contentReader: ContentReader!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
|
|
@ -1168,8 +1680,11 @@ type SearchItem {
|
|||
readAt: Date
|
||||
readingProgressAnchorIndex: Int!
|
||||
readingProgressPercent: Float!
|
||||
readingProgressTopPercent: Float
|
||||
recommendations: [Recommendation!]
|
||||
savedAt: Date!
|
||||
shortId: String
|
||||
siteIcon: String
|
||||
siteName: String
|
||||
slug: String!
|
||||
state: ArticleSavingRequestStatus
|
||||
|
|
@ -1180,6 +1695,7 @@ type SearchItem {
|
|||
updatedAt: Date
|
||||
uploadFileId: ID
|
||||
url: String!
|
||||
wordsCount: Int
|
||||
}
|
||||
|
||||
type SearchItemEdge {
|
||||
|
|
@ -1287,8 +1803,9 @@ enum SetIntegrationErrorCode {
|
|||
input SetIntegrationInput {
|
||||
enabled: Boolean!
|
||||
id: ID
|
||||
name: String!
|
||||
token: String!
|
||||
type: IntegrationType!
|
||||
type: IntegrationType
|
||||
}
|
||||
|
||||
union SetIntegrationResult = SetIntegrationError | SetIntegrationSuccess
|
||||
|
|
@ -1323,6 +1840,31 @@ type SetLabelsSuccess {
|
|||
labels: [Label!]!
|
||||
}
|
||||
|
||||
type SetRuleError {
|
||||
errorCodes: [SetRuleErrorCode!]!
|
||||
}
|
||||
|
||||
enum SetRuleErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input SetRuleInput {
|
||||
actions: [RuleActionInput!]!
|
||||
description: String
|
||||
enabled: Boolean!
|
||||
filter: String!
|
||||
id: ID
|
||||
name: String!
|
||||
}
|
||||
|
||||
union SetRuleResult = SetRuleError | SetRuleSuccess
|
||||
|
||||
type SetRuleSuccess {
|
||||
rule: Rule!
|
||||
}
|
||||
|
||||
type SetShareArticleError {
|
||||
errorCodes: [SetShareArticleErrorCode!]!
|
||||
}
|
||||
|
|
@ -1373,6 +1915,7 @@ type SetUserPersonalizationError {
|
|||
}
|
||||
|
||||
enum SetUserPersonalizationErrorCode {
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
|
|
@ -1382,6 +1925,10 @@ input SetUserPersonalizationInput {
|
|||
libraryLayoutType: String
|
||||
librarySortOrder: SortOrder
|
||||
margin: Int
|
||||
speechRate: String
|
||||
speechSecondaryVoice: String
|
||||
speechVoice: String
|
||||
speechVolume: String
|
||||
theme: String
|
||||
}
|
||||
|
||||
|
|
@ -1485,6 +2032,7 @@ type SubscribeSuccess {
|
|||
type Subscription {
|
||||
createdAt: Date!
|
||||
description: String
|
||||
icon: String
|
||||
id: ID!
|
||||
name: String!
|
||||
newsletterEmail: String!
|
||||
|
|
@ -1576,6 +2124,8 @@ enum UpdateHighlightErrorCode {
|
|||
input UpdateHighlightInput {
|
||||
annotation: String
|
||||
highlightId: ID!
|
||||
html: String
|
||||
quote: String
|
||||
sharedAt: Date
|
||||
}
|
||||
|
||||
|
|
@ -1664,8 +2214,11 @@ enum UpdatePageErrorCode {
|
|||
}
|
||||
|
||||
input UpdatePageInput {
|
||||
byline: String
|
||||
description: String
|
||||
pageId: ID!
|
||||
publishedAt: Date
|
||||
savedAt: Date
|
||||
title: String
|
||||
}
|
||||
|
||||
|
|
@ -1818,6 +2371,28 @@ enum UploadFileStatus {
|
|||
INITIALIZED
|
||||
}
|
||||
|
||||
type UploadImportFileError {
|
||||
errorCodes: [UploadImportFileErrorCode!]!
|
||||
}
|
||||
|
||||
enum UploadImportFileErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
UPLOAD_DAILY_LIMIT_EXCEEDED
|
||||
}
|
||||
|
||||
union UploadImportFileResult = UploadImportFileError | UploadImportFileSuccess
|
||||
|
||||
type UploadImportFileSuccess {
|
||||
uploadSignedUrl: String
|
||||
}
|
||||
|
||||
enum UploadImportFileType {
|
||||
MATTER
|
||||
POCKET
|
||||
URL_LIST
|
||||
}
|
||||
|
||||
type User {
|
||||
followersCount: Int
|
||||
friendsCount: Int
|
||||
|
|
@ -1851,6 +2426,10 @@ type UserPersonalization {
|
|||
libraryLayoutType: String
|
||||
librarySortOrder: SortOrder
|
||||
margin: Int
|
||||
speechRate: String
|
||||
speechSecondaryVoice: String
|
||||
speechVoice: String
|
||||
speechVolume: String
|
||||
theme: String
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.core.view.WindowCompat
|
|||
import androidx.core.view.WindowInsetsCompat
|
||||
import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.ui.library.LibraryViewModel
|
||||
import app.omnivore.omnivore.ui.library.SearchViewModel
|
||||
import app.omnivore.omnivore.ui.root.RootView
|
||||
import app.omnivore.omnivore.ui.settings.SettingsViewModel
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
|
|
@ -33,6 +34,7 @@ class MainActivity : ComponentActivity() {
|
|||
val loginViewModel: LoginViewModel by viewModels()
|
||||
val libraryViewModel: LibraryViewModel by viewModels()
|
||||
val settingsViewModel: SettingsViewModel by viewModels()
|
||||
val searchViewModel: SearchViewModel by viewModels()
|
||||
|
||||
val context = this
|
||||
|
||||
|
|
@ -53,7 +55,7 @@ class MainActivity : ComponentActivity() {
|
|||
.fillMaxSize()
|
||||
.background(color = Color.Black)
|
||||
) {
|
||||
RootView(loginViewModel, libraryViewModel, settingsViewModel)
|
||||
RootView(loginViewModel, searchViewModel, libraryViewModel, settingsViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package app.omnivore.omnivore
|
|||
sealed class Routes(val route: String) {
|
||||
object Library : Routes("Library")
|
||||
object Settings: Routes("Settings")
|
||||
object Search: Routes("Search")
|
||||
object Documentation: Routes("Documentation")
|
||||
object PrivacyPolicy: Routes("PrivacyPolicy")
|
||||
object TermsAndConditions: Routes("TermsAndConditions")
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ suspend fun DataService.createWebHighlight(jsonString: String) {
|
|||
val highlight = Highlight(
|
||||
highlightId = createHighlightInput.id,
|
||||
shortId = createHighlightInput.shortId,
|
||||
quote = createHighlightInput.quote,
|
||||
quote = createHighlightInput.quote.getOrNull(),
|
||||
prefix = null,
|
||||
suffix = null,
|
||||
patch = createHighlightInput.patch,
|
||||
patch = createHighlightInput.patch.getOrNull(),
|
||||
annotation = createHighlightInput.annotation.getOrNull(),
|
||||
createdAt = null,
|
||||
updatedAt = null,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.PrimaryKey
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
|
||||
suspend fun DataService.librarySearch(cursor: String?, query: String): SavedItemSyncResult {
|
||||
suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult {
|
||||
val searchResult = networker.search(cursor = cursor, limit = 10, query = query)
|
||||
|
||||
val savedItems = searchResult.items.map { it.item }
|
||||
val savedItems = searchResult.items.map {
|
||||
SavedItemWithLabelsAndHighlights(
|
||||
savedItem = it.item,
|
||||
labels = it.labels,
|
||||
highlights = it.highlights,
|
||||
)
|
||||
}
|
||||
|
||||
db.savedItemDao().insertAll(savedItems)
|
||||
db.savedItemDao().insertAll(savedItems.map { it.savedItem })
|
||||
|
||||
val labels: MutableList<SavedItemLabel> = mutableListOf()
|
||||
val crossRefs: MutableList<SavedItemAndSavedItemLabelCrossRef> = mutableListOf()
|
||||
|
|
@ -28,19 +36,20 @@ suspend fun DataService.librarySearch(cursor: String?, query: String): SavedItem
|
|||
db.savedItemLabelDao().insertAll(labels)
|
||||
db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
|
||||
|
||||
Log.d("sync", "found ${searchResult.items.size} items with search api. Query: $query")
|
||||
Log.d("sync", "found ${searchResult.items.size} items with search api. Query: $query cursor: $cursor")
|
||||
|
||||
return SavedItemSyncResult(
|
||||
return SearchResult(
|
||||
hasError = false,
|
||||
hasMoreItems = false,
|
||||
cursor = searchResult.cursor,
|
||||
count = searchResult.items.size,
|
||||
savedItemSlugs = savedItems.map { it.slug }
|
||||
savedItems = savedItems
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult {
|
||||
val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncResult.errorResult
|
||||
val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since)
|
||||
?: return SavedItemSyncResult.errorResult
|
||||
|
||||
val savedItems = syncResult.items.map {
|
||||
SavedItem(
|
||||
|
|
@ -62,7 +71,8 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
|
|||
slug = it.slug,
|
||||
isArchived = it.isArchived,
|
||||
contentReader = it.contentReader.rawValue,
|
||||
content = null
|
||||
content = null,
|
||||
wordsCount = it.wordsCount
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -75,9 +85,9 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
|
|||
for (item in syncResult.items) {
|
||||
val itemLabels = (item.labels ?: listOf()).map {
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = it.id,
|
||||
name = it.name,
|
||||
color = it.color,
|
||||
savedItemLabelId = it.labelFields.id,
|
||||
name = it.labelFields.name,
|
||||
color = it.labelFields.color,
|
||||
createdAt = null,
|
||||
labelDescription = null
|
||||
)
|
||||
|
|
@ -86,7 +96,10 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
|
|||
labels.addAll(itemLabels)
|
||||
|
||||
val newCrossRefs = itemLabels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = item.id)
|
||||
SavedItemAndSavedItemLabelCrossRef(
|
||||
savedItemLabelId = it.savedItemLabelId,
|
||||
savedItemId = item.id
|
||||
)
|
||||
}
|
||||
|
||||
crossRefs.addAll(newCrossRefs)
|
||||
|
|
@ -95,6 +108,37 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
|
|||
db.savedItemLabelDao().insertAll(labels)
|
||||
db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
|
||||
|
||||
// Persist Highlights
|
||||
db.highlightDao().insertAll(syncResult.items.flatMap {
|
||||
it.highlights ?: listOf()
|
||||
}.map {
|
||||
Highlight(
|
||||
highlightId = it.highlightFields.id,
|
||||
annotation = it.highlightFields.annotation,
|
||||
createdByMe = it.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
patch = it.highlightFields.patch,
|
||||
prefix = it.highlightFields.prefix,
|
||||
quote = it.highlightFields.quote,
|
||||
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
shortId = it.highlightFields.shortId,
|
||||
suffix = it.highlightFields.suffix,
|
||||
createdAt = null,
|
||||
updatedAt = it.highlightFields.updatedAt as String?,
|
||||
)
|
||||
})
|
||||
|
||||
val highlightCrossRefs = syncResult.items.flatMap {
|
||||
val savedItem = it
|
||||
(savedItem.highlights ?: listOf()).map {
|
||||
Pair(it, savedItem.id)
|
||||
}
|
||||
}.map {
|
||||
SavedItemAndHighlightCrossRef(highlightId = it.first.highlightFields.id, savedItemId = it.second)
|
||||
}
|
||||
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs)
|
||||
|
||||
Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since")
|
||||
|
||||
return SavedItemSyncResult(
|
||||
|
|
@ -150,3 +194,15 @@ data class SavedItemSyncResult(
|
|||
val errorResult = SavedItemSyncResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItemSlugs = listOf())
|
||||
}
|
||||
}
|
||||
|
||||
data class SearchResult(
|
||||
val hasError: Boolean,
|
||||
val hasMoreItems: Boolean,
|
||||
val count: Int,
|
||||
val savedItems: List<SavedItemWithLabelsAndHighlights>,
|
||||
val cursor: String?
|
||||
) {
|
||||
companion object {
|
||||
val errorResult = SearchResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItems = listOf())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,13 @@ import app.omnivore.omnivore.networking.*
|
|||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.apollographql.apollo3.api.Optional.Companion.absent
|
||||
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
suspend fun DataService.startSyncChannels() {
|
||||
for (savedItem in savedItemSyncChannel) {
|
||||
|
|
@ -130,8 +133,8 @@ private suspend fun DataService.syncHighlight(highlight: Highlight) {
|
|||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
articleId = savedItemID ?: "",
|
||||
id = highlight.highlightId,
|
||||
patch = highlight.patch ?: "",
|
||||
quote = highlight.quote ?: "",
|
||||
patch = Optional.presentIfNotNull(highlight.patch),
|
||||
quote = Optional.presentIfNotNull(highlight.quote),
|
||||
shortId = highlight.shortId ?: ""
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ data class CreateHighlightParams(
|
|||
annotation = Optional.presentIfNotNull(`annotation`),
|
||||
articleId = articleId ?: "",
|
||||
id = id ?: "",
|
||||
patch = patch ?: "",
|
||||
quote = quote ?: "",
|
||||
patch = Optional.presentIfNotNull(patch),
|
||||
quote = Optional.presentIfNotNull(quote),
|
||||
shortId = shortId ?: ""
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
|
|||
slug = article.articleFields.slug,
|
||||
isArchived = article.articleFields.isArchived,
|
||||
contentReader = article.articleFields.contentReader.rawValue,
|
||||
content = article.articleFields.content
|
||||
content = article.articleFields.content,
|
||||
wordsCount = article.articleFields.wordsCount
|
||||
)
|
||||
|
||||
return SavedItemQueryResponse(item = savedItem, highlights, labels = savedItemLabels, state = article.articleFields.state?.rawValue ?: "")
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import androidx.room.PrimaryKey
|
||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||
import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
data class SearchQueryResponse(
|
||||
val cursor: String?,
|
||||
val cardsData: List<SavedItemCardData>
|
||||
)
|
||||
|
||||
data class LibrarySearchQueryResponse(
|
||||
val cursor: String?,
|
||||
val items: List<LibrarySearchItem>
|
||||
|
|
@ -19,39 +14,10 @@ data class LibrarySearchQueryResponse(
|
|||
|
||||
data class LibrarySearchItem(
|
||||
val item: SavedItem,
|
||||
val labels: List<SavedItemLabel>
|
||||
val labels: List<SavedItemLabel>,
|
||||
val highlights: List<Highlight>
|
||||
)
|
||||
|
||||
suspend fun Networker.typeaheadSearch(
|
||||
query: String
|
||||
): SearchQueryResponse {
|
||||
try {
|
||||
val result = authenticatedApolloClient().query(
|
||||
TypeaheadSearchQuery(query)
|
||||
).execute()
|
||||
|
||||
val itemList = result.data?.typeaheadSearch?.onTypeaheadSearchSuccess?.items ?: listOf()
|
||||
|
||||
val cardsData = itemList.map {
|
||||
SavedItemCardData(
|
||||
savedItemId = it.id,
|
||||
slug = it.slug,
|
||||
publisherURLString = "",
|
||||
title = it.title,
|
||||
author = "",
|
||||
imageURLString = null,
|
||||
isArchived = false,
|
||||
pageURLString = "",
|
||||
contentReader = null,
|
||||
)
|
||||
}
|
||||
|
||||
return SearchQueryResponse(null, cardsData)
|
||||
} catch (e: java.lang.Exception) {
|
||||
return SearchQueryResponse(null, listOf())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Networker.search(
|
||||
cursor: String? = null,
|
||||
limit: Int = 15,
|
||||
|
|
@ -90,16 +56,32 @@ suspend fun Networker.search(
|
|||
slug = it.node.slug,
|
||||
isArchived = it.node.isArchived,
|
||||
contentReader = it.node.contentReader.rawValue,
|
||||
content = null
|
||||
content = null,
|
||||
wordsCount = it.node.wordsCount,
|
||||
),
|
||||
labels = (it.node.labels ?: listOf()).map { label ->
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = label.id,
|
||||
name = label.name,
|
||||
color = label.color,
|
||||
savedItemLabelId = label.labelFields.id,
|
||||
name = label.labelFields.name,
|
||||
color = label.labelFields.color,
|
||||
createdAt = null,
|
||||
labelDescription = null
|
||||
)
|
||||
},
|
||||
highlights = (it.node.highlights ?: listOf()).map { highlight ->
|
||||
Highlight(
|
||||
highlightId = highlight.highlightFields.id,
|
||||
annotation = highlight.highlightFields.annotation,
|
||||
createdByMe = highlight.highlightFields.createdByMe,
|
||||
patch = highlight.highlightFields.patch,
|
||||
prefix = highlight.highlightFields.prefix,
|
||||
quote = highlight.highlightFields.quote,
|
||||
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
shortId = highlight.highlightFields.shortId,
|
||||
suffix = highlight.highlightFields.suffix,
|
||||
updatedAt = highlight.highlightFields.updatedAt as String?,
|
||||
createdAt = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||
import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.TypeaheadCardData
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
data class SearchQueryResponse(
|
||||
val cursor: String?,
|
||||
val cardsData: List<TypeaheadCardData>
|
||||
)
|
||||
|
||||
suspend fun Networker.typeaheadSearch(
|
||||
query: String
|
||||
): SearchQueryResponse {
|
||||
try {
|
||||
val result = authenticatedApolloClient().query(
|
||||
TypeaheadSearchQuery(query)
|
||||
).execute()
|
||||
|
||||
val itemList = result.data?.typeaheadSearch?.onTypeaheadSearchSuccess?.items ?: listOf()
|
||||
|
||||
val cardsData = itemList.map {
|
||||
TypeaheadCardData(
|
||||
savedItemId = it.id,
|
||||
slug = it.slug,
|
||||
title = it.title,
|
||||
isArchived = false,
|
||||
)
|
||||
}
|
||||
|
||||
return SearchQueryResponse(null, cardsData)
|
||||
} catch (e: java.lang.Exception) {
|
||||
return SearchQueryResponse(null, listOf())
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import app.omnivore.omnivore.persistence.entities.*
|
|||
SavedItemAndSavedItemLabelCrossRef::class,
|
||||
SavedItemAndHighlightCrossRef::class
|
||||
],
|
||||
version = 3
|
||||
version = 5
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun viewerDao(): ViewerDao
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ data class Highlight(
|
|||
val createdAt: String?,
|
||||
val createdByMe: Boolean,
|
||||
val markedForDeletion: Boolean = false,
|
||||
var patch: String,
|
||||
var patch: String?,
|
||||
var prefix: String?,
|
||||
var quote: String,
|
||||
var quote: String?,
|
||||
var serverSyncStatus: Int = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
var shortId: String,
|
||||
val suffix: String?,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package app.omnivore.omnivore.persistence.entities
|
|||
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.*
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.ui.library.SavedItemSortFilter
|
||||
import java.util.*
|
||||
|
|
@ -40,7 +42,8 @@ data class SavedItem(
|
|||
val originalHtml: String? = null,
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) val pdfData: ByteArray? = null,
|
||||
var serverSyncStatus: Int = 0,
|
||||
val tempPDFURL: String? = null
|
||||
val tempPDFURL: String? = null,
|
||||
val wordsCount: Int? = null
|
||||
|
||||
// hasMany highlights
|
||||
// hasMany labels
|
||||
|
|
@ -75,7 +78,10 @@ data class SavedItemCardData(
|
|||
val imageURLString: String?,
|
||||
val isArchived: Boolean,
|
||||
val pageURLString: String,
|
||||
val contentReader: String?
|
||||
val contentReader: String?,
|
||||
val savedAt: String,
|
||||
val readingProgress: Double,
|
||||
val wordsCount: Int?
|
||||
) {
|
||||
fun publisherDisplayName(): String? {
|
||||
return publisherURLString?.toUri()?.host
|
||||
|
|
@ -87,6 +93,13 @@ data class SavedItemCardData(
|
|||
}
|
||||
}
|
||||
|
||||
data class TypeaheadCardData(
|
||||
val savedItemId: String,
|
||||
val slug: String,
|
||||
val title: String,
|
||||
val isArchived: Boolean,
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface SavedItemDao {
|
||||
@Query("SELECT * FROM savedItem")
|
||||
|
|
@ -148,9 +161,73 @@ interface SavedItemDao {
|
|||
"ORDER BY publishDate DESC"
|
||||
)
|
||||
fun getLibraryLiveDataSortedByRecentlyPublished(archiveFilter: Int): LiveData<List<SavedItemCardDataWithLabels>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.serverSyncStatus != 2 " +
|
||||
"AND SavedItem.isArchived != :archiveFilter " +
|
||||
"AND SavedItem.contentReader IN (:allowedContentReaders) " +
|
||||
"AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " +
|
||||
"AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId " +
|
||||
|
||||
"ORDER BY \n" +
|
||||
"CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" +
|
||||
"CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" +
|
||||
|
||||
"CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" +
|
||||
"CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC"
|
||||
)
|
||||
fun _filteredLibraryData(archiveFilter: Int, sortKey: String, hasRequiredLabels: Int, hasExcludedLabels: Int, requiredLabels: List<String>, excludedLabels: List<String>, allowedContentReaders: List<String>): LiveData<List<SavedItemWithLabelsAndHighlights>>
|
||||
|
||||
fun filteredLibraryData(archiveFilter: Int, sortKey: String, requiredLabels: List<String>, excludedLabels: List<String>, allowedContentReaders: List<String>): LiveData<List<SavedItemWithLabelsAndHighlights>> {
|
||||
return _filteredLibraryData(
|
||||
archiveFilter = archiveFilter,
|
||||
sortKey = sortKey,
|
||||
hasRequiredLabels = requiredLabels.size,
|
||||
hasExcludedLabels = excludedLabels.size,
|
||||
requiredLabels = requiredLabels,
|
||||
excludedLabels = excludedLabels,
|
||||
allowedContentReaders = allowedContentReaders
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
object SavedItemQueryConstants {
|
||||
const val columns = "savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader "
|
||||
const val columns = "savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader, savedAt, readingProgress, wordsCount"
|
||||
const val libraryColumns = "SavedItem.savedItemId, " +
|
||||
"SavedItem.slug, " +
|
||||
"SavedItem.createdAt, " +
|
||||
|
||||
"SavedItem.publisherURLString, " +
|
||||
"SavedItem.title, " +
|
||||
"SavedItem.author, " +
|
||||
"SavedItem.imageURLString, " +
|
||||
"SavedItem.isArchived, " +
|
||||
"SavedItem.pageURLString, " +
|
||||
"SavedItem.contentReader, " +
|
||||
"SavedItem.savedAt, " +
|
||||
"SavedItem.readingProgress, " +
|
||||
"SavedItem.readingProgressAnchor, " +
|
||||
"SavedItem.serverSyncStatus, " +
|
||||
|
||||
"SavedItem.wordsCount, " +
|
||||
"SavedItemLabel.savedItemLabelId, " +
|
||||
"SavedItemLabel.name, " +
|
||||
"SavedItemLabel.color, " +
|
||||
"Highlight.highlightId, " +
|
||||
"Highlight.shortId, " +
|
||||
"Highlight.createdByMe "
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalUriHandler
|
|||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -113,7 +114,10 @@ fun LoginFields(
|
|||
placeholder = { Text(text = "user@email.com") },
|
||||
label = { Text(text = "Email") },
|
||||
onValueChange = onEmailChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Email,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
|
|
@ -123,7 +127,10 @@ fun LoginFields(
|
|||
label = { Text(text = "Password") },
|
||||
onValueChange = onPasswordChange,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Password,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ fun LabelsSelectionSheet(viewModel: LibraryViewModel) {
|
|||
isLibraryMode = false,
|
||||
onSave = {
|
||||
if (it != labels) {
|
||||
viewModel.updateSavedItemLabels(savedItemID = currentSavedItemData.cardData.savedItemId, labels = it)
|
||||
viewModel.updateSavedItemLabels(savedItemID = currentSavedItemData.savedItem.savedItemId, labels = it)
|
||||
}
|
||||
viewModel.labelsSelectionCurrentItemLiveData.value = null
|
||||
viewModel.showLabelsSelectionSheetLiveData.value = false
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.ui.components.LabelChipColors
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,267 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LibraryNavigationBar(
|
||||
savedItemViewModel: SavedItemViewModel,
|
||||
onSearchClicked: () -> Unit,
|
||||
onSettingsIconClick: () -> Unit
|
||||
) {
|
||||
val actionsMenuItem: SavedItemWithLabelsAndHighlights? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null)
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(if (actionsMenuItem == null) "Library" else "")
|
||||
},
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = if (actionsMenuItem == null) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
navigationIcon = {
|
||||
if (actionsMenuItem != null) {
|
||||
IconButton(onClick = {
|
||||
savedItemViewModel.actionsMenuItemLiveData.postValue(null)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
actionsMenuItem?.let {
|
||||
IconButton(onClick = {
|
||||
savedItemViewModel.handleSavedItemAction(
|
||||
it.savedItem.savedItemId,
|
||||
if (it.savedItem.isArchived) SavedItemAction.Unarchive else SavedItemAction.Archive
|
||||
) }) {
|
||||
if (it.savedItem.isArchived) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.unarchive),
|
||||
contentDescription = null
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.archive_outline),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { savedItemViewModel.handleSavedItemAction(it.savedItem.savedItemId, SavedItemAction.EditLabels) }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.tag),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { savedItemViewModel.handleSavedItemAction(it.savedItem.savedItemId, SavedItemAction.Delete) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
// IconButton(onClick = onSettingsIconClick) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Default.MoreVert,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
} ?: run {
|
||||
IconButton(onClick = onSearchClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onSettingsIconClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
//
|
||||
//@OptIn(ExperimentalMaterial3Api::class)
|
||||
//@Composable
|
||||
//fun LibraryBottomBar(
|
||||
// savedItemViewModel: SavedItemViewModel,
|
||||
// onSearchClicked: () -> Unit,
|
||||
// onSettingsIconClick: () -> Unit
|
||||
//) {
|
||||
// val actionsMenuItem: SavedItemCardData? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null)
|
||||
//
|
||||
// BottomAppBar(
|
||||
//// colors = TopAppBarDefaults.topAppBarColors(
|
||||
//// containerColor = if (actionsMenuItem == null) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.surfaceVariant
|
||||
//// ),
|
||||
//// trailingIcon = {
|
||||
//// if (actionsMenuItem != null) {
|
||||
//// IconButton(onClick = {
|
||||
//// savedItemViewModel.actionsMenuItemLiveData.postValue(null)
|
||||
//// }) {
|
||||
//// Icon(
|
||||
//// imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack,
|
||||
//// modifier = Modifier,
|
||||
//// contentDescription = "Back"
|
||||
//// )
|
||||
//// }
|
||||
//// }
|
||||
//// },
|
||||
// floatingActionButton = {
|
||||
// IconButton(onClick = onSearchClicked) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Outlined.Close,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
// },
|
||||
// actions = {
|
||||
// actionsMenuItem?.let {
|
||||
// IconButton(onClick = onSearchClicked) {
|
||||
// if (it.isArchived) {
|
||||
// Icon(
|
||||
// painter = painterResource(id = R.drawable.unarchive),
|
||||
// contentDescription = null
|
||||
// )
|
||||
// } else {
|
||||
// Icon(
|
||||
// painter = painterResource(id = R.drawable.archive_outline),
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// IconButton(onClick = onSearchClicked) {
|
||||
// Icon(
|
||||
// painter = painterResource(id = R.drawable.tag),
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
// IconButton(onClick = onSearchClicked) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Outlined.Delete,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
// IconButton(onClick = onSettingsIconClick) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Default.MoreVert,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
// } ?: run {
|
||||
// IconButton(onClick = onSearchClicked) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Filled.Search,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// IconButton(onClick = onSettingsIconClick) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Default.MoreVert,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
//}
|
||||
|
||||
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchField(
|
||||
searchText: String,
|
||||
onSearch: () -> Unit,
|
||||
onSearchTextChanged: (String) -> Unit,
|
||||
navController: NavHostController,
|
||||
) {
|
||||
var showClearButton by remember { mutableStateOf(false) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { focusState ->
|
||||
showClearButton = (focusState.isFocused)
|
||||
}
|
||||
.focusRequester(focusRequester),
|
||||
value = searchText,
|
||||
onValueChange = onSearchTextChanged,
|
||||
placeholder = {
|
||||
Text(text = "Search")
|
||||
},
|
||||
leadingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onSearchTextChanged("")
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
AnimatedVisibility(
|
||||
visible = showClearButton,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
IconButton(onClick = { onSearchTextChanged("") }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = {
|
||||
onSearch()
|
||||
}),
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,30 +2,45 @@ package app.omnivore.omnivore.ui.library
|
|||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetLayout
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.ui.components.LabelsSelectionSheet
|
||||
import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard
|
||||
import app.omnivore.omnivore.ui.reader.PDFReaderActivity
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity
|
||||
import app.omnivore.omnivore.ui.save.SaveSheetActivityBase
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
|
||||
|
|
@ -37,24 +52,25 @@ fun LibraryView(
|
|||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchBar(
|
||||
libraryViewModel = libraryViewModel,
|
||||
LibraryNavigationBar(
|
||||
savedItemViewModel = libraryViewModel,
|
||||
onSearchClicked = { navController.navigate(Routes.Search.route) },
|
||||
onSettingsIconClick = { navController.navigate(Routes.Settings.route) }
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
) { paddingValues ->
|
||||
LibraryViewContent(
|
||||
libraryViewModel,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding()
|
||||
)
|
||||
)
|
||||
LibraryViewContent(
|
||||
libraryViewModel,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -65,8 +81,9 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
|||
onRefresh = { libraryViewModel.refresh() }
|
||||
)
|
||||
|
||||
val cardsData: List<SavedItemCardDataWithLabels> by libraryViewModel.itemsLiveData.observeAsState(listOf())
|
||||
val searchedCardsData: List<SavedItemCardDataWithLabels> by libraryViewModel.searchItemsLiveData.observeAsState(listOf())
|
||||
val cardsData: List<SavedItemWithLabelsAndHighlights> by libraryViewModel.itemsLiveData.observeAsState(
|
||||
listOf()
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -82,22 +99,26 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
|||
.fillMaxSize()
|
||||
.padding(horizontal = 6.dp)
|
||||
) {
|
||||
if (!libraryViewModel.showSearchField) {
|
||||
item {
|
||||
LibraryFilterBar(libraryViewModel)
|
||||
}
|
||||
item {
|
||||
LibraryFilterBar(libraryViewModel)
|
||||
}
|
||||
items(if (libraryViewModel.showSearchField) searchedCardsData else cardsData) { cardDataWithLabels ->
|
||||
items(cardsData) { cardDataWithLabels ->
|
||||
SavedItemCard(
|
||||
cardData = cardDataWithLabels.cardData,
|
||||
labels = cardDataWithLabels.labels,
|
||||
savedItemViewModel = libraryViewModel,
|
||||
savedItem = cardDataWithLabels,
|
||||
onClickHandler = {
|
||||
val activityClass = if (cardDataWithLabels.cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java
|
||||
val activityClass =
|
||||
if (cardDataWithLabels.savedItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java
|
||||
val intent = Intent(context, activityClass)
|
||||
intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.cardData.slug)
|
||||
intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.savedItem.slug)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
actionHandler = { libraryViewModel.handleSavedItemAction(cardDataWithLabels.cardData.savedItemId, it) }
|
||||
actionHandler = {
|
||||
libraryViewModel.handleSavedItemAction(
|
||||
cardDataWithLabels.savedItem.savedItemId,
|
||||
it
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -111,7 +132,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
|||
libraryViewModel.loadUsingSearchAPI()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = libraryViewModel.isRefreshing,
|
||||
state = pullRefreshState,
|
||||
|
|
|
|||
|
|
@ -4,16 +4,14 @@ import android.util.Log
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.*
|
||||
import app.omnivore.omnivore.*
|
||||
import app.omnivore.omnivore.dataService.*
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
|
@ -25,7 +23,7 @@ class LibraryViewModel @Inject constructor(
|
|||
private val networker: Networker,
|
||||
private val dataService: DataService,
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
): ViewModel() {
|
||||
): ViewModel(), SavedItemViewModel {
|
||||
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
|
||||
|
||||
private var cursor: String? = null
|
||||
|
|
@ -37,10 +35,14 @@ class LibraryViewModel @Inject constructor(
|
|||
private var receivedIdx = 0
|
||||
|
||||
// Live Data
|
||||
val searchTextLiveData = MutableLiveData("")
|
||||
val searchItemsLiveData = MutableLiveData<List<SavedItemCardDataWithLabels>>(listOf())
|
||||
private var itemsLiveDataInternal = dataService.libraryLiveData(SavedItemFilter.INBOX, SavedItemSortFilter.NEWEST, listOf())
|
||||
val itemsLiveData = MediatorLiveData<List<SavedItemCardDataWithLabels>>()
|
||||
private var itemsLiveDataInternal = dataService.db.savedItemDao().filteredLibraryData(
|
||||
archiveFilter = 1,
|
||||
sortKey = "newest",
|
||||
requiredLabels = listOf(),
|
||||
excludedLabels = listOf(),
|
||||
allowedContentReaders = listOf("WEB", "PDF", "EPUB")
|
||||
)
|
||||
val itemsLiveData = MediatorLiveData<List<SavedItemWithLabelsAndHighlights>>()
|
||||
val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX)
|
||||
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
|
||||
val showLabelsSelectionSheetLiveData = MutableLiveData(false)
|
||||
|
|
@ -48,8 +50,9 @@ class LibraryViewModel @Inject constructor(
|
|||
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
|
||||
val activeLabelsLiveData = MutableLiveData<List<SavedItemLabel>>(listOf())
|
||||
|
||||
override val actionsMenuItemLiveData = MutableLiveData<SavedItemWithLabelsAndHighlights?>(null)
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
var showSearchField by mutableStateOf(false)
|
||||
var hasLoadedInitialFilters = false
|
||||
|
||||
fun loadInitialFilterValues() {
|
||||
|
|
@ -62,25 +65,27 @@ class LibraryViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemFilter)?.let { str ->
|
||||
try {
|
||||
val filter = SavedItemFilter.values().first { it.rawValue == str }
|
||||
appliedFilterLiveData.postValue(filter)
|
||||
} catch (e: Exception) {
|
||||
Log.d("error", "invalid filter value stored in datastore repo: $e")
|
||||
}
|
||||
}
|
||||
|
||||
datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemSortFilter)?.let { str ->
|
||||
try {
|
||||
val filter = SavedItemSortFilter.values().first { it.rawValue == str }
|
||||
appliedSortFilterLiveData.postValue(filter)
|
||||
} catch (e: Exception) {
|
||||
Log.d("error", "invalid sort filter value stored in datastore repo: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
// runBlocking {
|
||||
// datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemFilter)?.let { str ->
|
||||
// try {
|
||||
// val filter = SavedItemFilter.values().first { it.rawValue == str }
|
||||
// appliedFilterLiveData.postValue(filter)
|
||||
// } catch (e: Exception) {
|
||||
// Log.d("error", "invalid filter value stored in datastore repo: $e")
|
||||
// }
|
||||
//
|
||||
// datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemSortFilter)?.let { str ->
|
||||
// try {
|
||||
// val filter = SavedItemSortFilter.values().first { it.rawValue == str }
|
||||
// appliedSortFilterLiveData.postValue(filter)
|
||||
// } catch (e: Exception) {
|
||||
// Log.d("error", "invalid sort filter value stored in datastore repo: $e")
|
||||
// }
|
||||
//
|
||||
// handleFilterChanges()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
viewModelScope.launch {
|
||||
handleFilterChanges()
|
||||
|
|
@ -92,16 +97,6 @@ class LibraryViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSearchText(text: String) {
|
||||
searchTextLiveData.value = text
|
||||
|
||||
if (text == "") {
|
||||
searchItemsLiveData.value = listOf()
|
||||
} else {
|
||||
load(clearPreviousSearch = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
isRefreshing = true
|
||||
load(true)
|
||||
|
|
@ -134,12 +129,8 @@ class LibraryViewModel @Inject constructor(
|
|||
loadInitialFilterValues()
|
||||
|
||||
viewModelScope.launch {
|
||||
if (searchTextLiveData.value != "") {
|
||||
performSearch(clearPreviousSearch)
|
||||
} else {
|
||||
syncItems()
|
||||
loadUsingSearchAPI()
|
||||
}
|
||||
syncItems()
|
||||
loadUsingSearchAPI()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,12 +145,12 @@ class LibraryViewModel @Inject constructor(
|
|||
isRefreshing = false
|
||||
}
|
||||
|
||||
result.savedItemSlugs.map {
|
||||
val isSavedInDB = dataService.isSavedItemContentStoredInDB(it)
|
||||
result.savedItems.map {
|
||||
val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.savedItem.slug)
|
||||
|
||||
if (!isSavedInDB) {
|
||||
delay(2000)
|
||||
contentRequestChannel.send(it)
|
||||
contentRequestChannel.send(it.savedItem.slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -189,12 +180,56 @@ class LibraryViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun handleFilterChanges() {
|
||||
if (searchTextLiveData.value != "") {
|
||||
performSearch(true)
|
||||
} else if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) {
|
||||
itemsLiveDataInternal = dataService.libraryLiveData(appliedFilterLiveData.value!!, appliedSortFilterLiveData.value!!, activeLabelsLiveData.value ?: listOf())
|
||||
itemsLiveData.removeSource(itemsLiveDataInternal)
|
||||
fun sortKey(appliedSortKey: String) {
|
||||
when(appliedSortKey) {
|
||||
|
||||
}
|
||||
}
|
||||
fun handleFilterChanges() {
|
||||
if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) {
|
||||
val applied = appliedFilterLiveData.value
|
||||
val sortKey = when (appliedSortFilterLiveData.value) {
|
||||
SavedItemSortFilter.NEWEST -> "newest"
|
||||
SavedItemSortFilter.OLDEST -> "oldest"
|
||||
SavedItemSortFilter.RECENTLY_READ -> "recentlyRead"
|
||||
SavedItemSortFilter.RECENTLY_PUBLISHED -> "recentlyPublished"
|
||||
else -> "newest"
|
||||
}
|
||||
|
||||
val archiveFilter = when (appliedFilterLiveData.value) {
|
||||
SavedItemFilter.ARCHIVED -> 0
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val allowedContentReaders = when(appliedFilterLiveData.value) {
|
||||
SavedItemFilter.FILES -> listOf("PDF", "EPUB")
|
||||
else -> listOf("WEB", "PDF", "EPUB")
|
||||
}
|
||||
|
||||
var requiredLabels = when(appliedFilterLiveData.value) {
|
||||
SavedItemFilter.NEWSLETTERS -> listOf("Newsletter")
|
||||
else -> (activeLabelsLiveData.value ?: listOf()).map { it.name }
|
||||
}
|
||||
activeLabelsLiveData.value?.let {
|
||||
requiredLabels = requiredLabels + it.map { it.name }
|
||||
}
|
||||
|
||||
|
||||
val excludeLabels = when(appliedFilterLiveData.value) {
|
||||
SavedItemFilter.READ_LATER -> listOf("Newsletter")
|
||||
else -> listOf()
|
||||
}
|
||||
|
||||
val newData = dataService.db.savedItemDao().filteredLibraryData(
|
||||
archiveFilter = archiveFilter,
|
||||
sortKey = sortKey,
|
||||
requiredLabels = requiredLabels,
|
||||
excludedLabels = excludeLabels,
|
||||
allowedContentReaders = allowedContentReaders
|
||||
)
|
||||
|
||||
itemsLiveData.removeSource(itemsLiveDataInternal)
|
||||
itemsLiveDataInternal = newData
|
||||
itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -238,38 +273,7 @@ class LibraryViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun performSearch(clearPreviousSearch: Boolean) {
|
||||
if (clearPreviousSearch) {
|
||||
cursor = null
|
||||
}
|
||||
|
||||
val thisSearchIdx = searchIdx
|
||||
searchIdx += 1
|
||||
|
||||
// Execute the search
|
||||
val searchResult = networker.typeaheadSearch(searchTextLiveData.value ?: "")
|
||||
|
||||
// Search results aren't guaranteed to return in order so this
|
||||
// will discard old results that are returned while a user is typing.
|
||||
// For example if a user types 'Canucks', often the search results
|
||||
// for 'C' are returned after 'Canucks' because it takes the backend
|
||||
// much longer to compute.
|
||||
if (thisSearchIdx in 1..receivedIdx) {
|
||||
return
|
||||
}
|
||||
|
||||
val cardsDataWithLabels = searchResult.cardsData.map {
|
||||
SavedItemCardDataWithLabels(cardData = it, labels = listOf())
|
||||
}
|
||||
|
||||
searchItemsLiveData.postValue(cardsDataWithLabels)
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
|
||||
override fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
|
||||
when (action) {
|
||||
SavedItemAction.Delete -> {
|
||||
viewModelScope.launch {
|
||||
|
|
@ -291,6 +295,7 @@ class LibraryViewModel @Inject constructor(
|
|||
showLabelsSelectionSheetLiveData.value = true
|
||||
}
|
||||
}
|
||||
actionsMenuItemLiveData.postValue(null)
|
||||
}
|
||||
|
||||
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
|
||||
|
|
@ -323,7 +328,7 @@ class LibraryViewModel @Inject constructor(
|
|||
fun createNewSavedItemLabel(labelName: String, hexColorValue: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newLabel = networker.createNewLabel(CreateLabelInput(color = hexColorValue, name = labelName))
|
||||
val newLabel = networker.createNewLabel(CreateLabelInput(color = Optional.presentIfNotNull(hexColorValue), name = labelName))
|
||||
|
||||
newLabel?.let {
|
||||
val savedItemLabel = SavedItemLabel(
|
||||
|
|
@ -340,9 +345,9 @@ class LibraryViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun currentSavedItemUnderEdit(): SavedItemCardDataWithLabels? {
|
||||
fun currentSavedItemUnderEdit(): SavedItemWithLabelsAndHighlights? {
|
||||
labelsSelectionCurrentItemLiveData.value?.let { itemID ->
|
||||
return itemsLiveData.value?.first { it.cardData.savedItemId == itemID }
|
||||
return itemsLiveData.value?.first { it.savedItem.savedItemId == itemID }
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
@ -350,11 +355,6 @@ class LibraryViewModel @Inject constructor(
|
|||
|
||||
private fun searchQueryString(): String {
|
||||
var query = "${appliedFilterLiveData.value?.queryString} ${appliedSortFilterLiveData.value?.queryString}"
|
||||
val searchText = searchTextLiveData.value ?: ""
|
||||
|
||||
if (searchText.isNotEmpty()) {
|
||||
query += " $searchText"
|
||||
}
|
||||
|
||||
activeLabelsLiveData.value?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
|
||||
interface SavedItemViewModel {
|
||||
|
||||
val actionsMenuItemLiveData: MutableLiveData<SavedItemWithLabelsAndHighlights?>
|
||||
get() = MutableLiveData<SavedItemWithLabelsAndHighlights?>(null)
|
||||
|
||||
fun handleSavedItemAction(itemID: String, action: SavedItemAction)
|
||||
}
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
libraryViewModel: LibraryViewModel,
|
||||
onSettingsIconClick: () -> Unit
|
||||
) {
|
||||
val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("")
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
if (libraryViewModel.showSearchField) {
|
||||
SearchField(searchText) { libraryViewModel.updateSearchText(it) }
|
||||
} else {
|
||||
Text("Library")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
actions = {
|
||||
if (libraryViewModel.showSearchField) {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
libraryViewModel.updateSearchText("")
|
||||
libraryViewModel.showSearchField = false
|
||||
}
|
||||
.padding(horizontal = 6.dp)
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = { libraryViewModel.showSearchField = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onSettingsIconClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchField(
|
||||
searchText: String,
|
||||
onSearchTextChanged: (String) -> Unit
|
||||
) {
|
||||
var showClearButton by remember { mutableStateOf(false) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Row {
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp)
|
||||
.onFocusChanged { focusState ->
|
||||
showClearButton = (focusState.isFocused)
|
||||
}
|
||||
.focusRequester(focusRequester),
|
||||
value = searchText,
|
||||
onValueChange = onSearchTextChanged,
|
||||
placeholder = {
|
||||
Text(text = "Search")
|
||||
},
|
||||
trailingIcon = {
|
||||
AnimatedVisibility(
|
||||
visible = showClearButton,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
IconButton(onClick = { onSearchTextChanged("") }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboardController?.hide()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity
|
||||
import app.omnivore.omnivore.persistence.entities.TypeaheadCardData
|
||||
import app.omnivore.omnivore.ui.reader.PDFReaderActivity
|
||||
import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard
|
||||
import app.omnivore.omnivore.ui.savedItemViews.TypeaheadSearchCard
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchView(
|
||||
viewModel: SearchViewModel,
|
||||
navController: NavHostController
|
||||
) {
|
||||
val isRefreshing: Boolean by viewModel.isRefreshing.observeAsState(false)
|
||||
val typeaheadMode: Boolean by viewModel.typeaheadMode.observeAsState(true)
|
||||
val searchText: String by viewModel.searchTextLiveData.observeAsState("")
|
||||
val actionsMenuItem: SavedItemWithLabelsAndHighlights? by viewModel.actionsMenuItemLiveData.observeAsState(null)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
navigationIcon = {
|
||||
if (actionsMenuItem != null) {
|
||||
IconButton(onClick = {
|
||||
viewModel.actionsMenuItemLiveData.postValue(null)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (actionsMenuItem != null) {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.archive_outline),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.tag),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
SearchField(
|
||||
searchText,
|
||||
onSearch = {
|
||||
viewModel.performSearch()
|
||||
},
|
||||
onSearchTextChanged = { viewModel.updateSearchText(it) },
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (isRefreshing) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(45.dp)
|
||||
.width(45.dp),
|
||||
strokeWidth = 5.dp,
|
||||
color = colorResource(R.color.green_55B938)
|
||||
)
|
||||
}
|
||||
} else if (typeaheadMode) {
|
||||
TypeaheadSearchViewContent(
|
||||
viewModel,
|
||||
modifier = Modifier
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
)
|
||||
} else {
|
||||
SearchViewContent(
|
||||
viewModel,
|
||||
modifier = Modifier
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun TypeaheadSearchViewContent(viewModel: SearchViewModel, modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val searchedCardsData: List<TypeaheadCardData> by viewModel.searchItemsLiveData.observeAsState(listOf())
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
items(searchedCardsData) { cardData ->
|
||||
TypeaheadSearchCard(
|
||||
cardData = cardData,
|
||||
onClickHandler = {
|
||||
// val activityClass = if (cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java
|
||||
val activityClass = WebReaderLoadingContainerActivity::class.java
|
||||
val intent = Intent(context, activityClass)
|
||||
intent.putExtra("SAVED_ITEM_SLUG", cardData.slug)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
actionHandler = { viewModel.handleSavedItemAction(cardData.savedItemId, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun SearchViewContent(viewModel: SearchViewModel, modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val cardsData: List<SavedItemWithLabelsAndHighlights> by viewModel.itemsLiveData.observeAsState(listOf())
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
items(cardsData) { cardDataWithLabels ->
|
||||
SavedItemCard(
|
||||
savedItemViewModel = viewModel,
|
||||
savedItem = cardDataWithLabels,
|
||||
onClickHandler = {
|
||||
val activityClass = if (cardDataWithLabels.savedItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java
|
||||
val intent = Intent(context, activityClass)
|
||||
intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.savedItem.slug)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
actionHandler = { viewModel.handleSavedItemAction(cardDataWithLabels.savedItem.savedItemId, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.*
|
||||
import app.omnivore.omnivore.dataService.*
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchViewModel @Inject constructor(
|
||||
private val networker: Networker,
|
||||
private val dataService: DataService,
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
): ViewModel(), SavedItemViewModel {
|
||||
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
|
||||
|
||||
private var cursor: String? = null
|
||||
private var librarySearchCursor: String? = null
|
||||
|
||||
// These are used to make sure we handle search result
|
||||
// responses in the right order
|
||||
private var searchIdx = 0
|
||||
private var receivedIdx = 0
|
||||
|
||||
// Live Data
|
||||
var isRefreshing = MutableLiveData(false)
|
||||
val typeaheadMode = MutableLiveData(true)
|
||||
val searchTextLiveData = MutableLiveData("")
|
||||
val searchItemsLiveData = MutableLiveData<List<TypeaheadCardData>>(listOf())
|
||||
val itemsLiveData = MediatorLiveData<List<SavedItemWithLabelsAndHighlights>>()
|
||||
|
||||
override val actionsMenuItemLiveData = MutableLiveData<SavedItemWithLabelsAndHighlights?>(null)
|
||||
|
||||
fun updateSearchText(text: String) {
|
||||
typeaheadMode.postValue(true)
|
||||
searchTextLiveData.value = text
|
||||
|
||||
if (text == "") {
|
||||
searchItemsLiveData.value = listOf()
|
||||
} else {
|
||||
viewModelScope.launch {
|
||||
performTypeaheadSearch(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performSearch() {
|
||||
// To perform search we just clear the current state, so the LibraryView infinite scroll
|
||||
// load will update items.
|
||||
viewModelScope.launch {
|
||||
isRefreshing.postValue(true)
|
||||
itemsLiveData.postValue(listOf())
|
||||
typeaheadMode.postValue(false)
|
||||
|
||||
loadUsingSearchAPI()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadUsingSearchAPI() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = dataService.librarySearch(cursor = librarySearchCursor, query = searchQueryString())
|
||||
result.cursor?.let {
|
||||
librarySearchCursor = it
|
||||
}
|
||||
|
||||
result.savedItems.map {
|
||||
val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.savedItem.slug)
|
||||
|
||||
if (!isSavedInDB) {
|
||||
delay(2000)
|
||||
contentRequestChannel.send(it.savedItem.slug)
|
||||
}
|
||||
}
|
||||
|
||||
val newItems = result.savedItems
|
||||
/*
|
||||
.map {
|
||||
SavedItemCardDataWithLabels(
|
||||
cardData = SavedItemCardData(
|
||||
savedItemId = it.savedItem.savedItemId,
|
||||
slug = it.savedItem.slug,
|
||||
publisherURLString = it.savedItem.publisherURLString,
|
||||
title = it.savedItem.title,
|
||||
author = it.savedItem.author,
|
||||
imageURLString = it.savedItem.imageURLString,
|
||||
isArchived = it.savedItem.isArchived,
|
||||
pageURLString = it.savedItem.pageURLString,
|
||||
contentReader = it.savedItem.contentReader,
|
||||
savedAt = it.savedItem.savedAt,
|
||||
readingProgress = it.savedItem.readingProgress,
|
||||
wordsCount = it.savedItem.wordsCount
|
||||
),
|
||||
labels = listOf()
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
itemsLiveData.value?.let{
|
||||
itemsLiveData.postValue(newItems + it)
|
||||
} ?: run {
|
||||
itemsLiveData.postValue(newItems)
|
||||
}
|
||||
|
||||
isRefreshing.postValue(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performTypeaheadSearch(clearPreviousSearch: Boolean) {
|
||||
if (clearPreviousSearch) {
|
||||
cursor = null
|
||||
}
|
||||
|
||||
val thisSearchIdx = searchIdx
|
||||
searchIdx += 1
|
||||
|
||||
// Execute the search
|
||||
val searchResult = networker.typeaheadSearch(searchTextLiveData.value ?: "")
|
||||
|
||||
// Search results aren't guaranteed to return in order so this
|
||||
// will discard old results that are returned while a user is typing.
|
||||
// For example if a user types 'Canucks', often the search results
|
||||
// for 'C' are returned after 'Canucks' because it takes the backend
|
||||
// much longer to compute.
|
||||
if (thisSearchIdx in 1..receivedIdx) {
|
||||
return
|
||||
}
|
||||
|
||||
searchItemsLiveData.postValue(searchResult.cardsData)
|
||||
|
||||
isRefreshing.postValue(false)
|
||||
}
|
||||
|
||||
override fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
|
||||
when (action) {
|
||||
SavedItemAction.Delete -> {
|
||||
viewModelScope.launch {
|
||||
dataService.deleteSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
SavedItemAction.Archive -> {
|
||||
viewModelScope.launch {
|
||||
dataService.archiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
SavedItemAction.Unarchive -> {
|
||||
viewModelScope.launch {
|
||||
dataService.unarchiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
SavedItemAction.EditLabels -> {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
actionsMenuItemLiveData.postValue(null)
|
||||
}
|
||||
|
||||
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
|
||||
// viewModelScope.launch {
|
||||
// withContext(Dispatchers.IO) {
|
||||
// val input = SetLabelsInput(labelIds = labels.map { it.savedItemLabelId }, pageId = savedItemID)
|
||||
// val networkResult = networker.updateLabelsForSavedItem(input)
|
||||
//
|
||||
// // TODO: assign a server sync status to these
|
||||
// val crossRefs = labels.map {
|
||||
// SavedItemAndSavedItemLabelCrossRef(
|
||||
// savedItemLabelId = it.savedItemLabelId,
|
||||
// savedItemId = savedItemID
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // Remove all labels first
|
||||
// dataService.db.savedItemAndSavedItemLabelCrossRefDao().deleteRefsBySavedItemId(savedItemID)
|
||||
//
|
||||
// // Add back the current labels
|
||||
// dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
|
||||
//
|
||||
// CoroutineScope(Dispatchers.Main).launch {
|
||||
// handleFilterChanges()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private fun searchQueryString(): String {
|
||||
var query = ""
|
||||
val searchText = searchTextLiveData.value ?: ""
|
||||
|
||||
if (searchText.isNotEmpty()) {
|
||||
query += " $searchText"
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
|
@ -134,13 +134,16 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan
|
|||
|
||||
private fun loadHighlights(highlights: List<Highlight>) {
|
||||
for (highlight in highlights) {
|
||||
val highlightAnnotation = fragment
|
||||
.document
|
||||
?.annotationProvider
|
||||
?.createAnnotationFromInstantJson(highlight.patch)
|
||||
val patch = highlight.patch
|
||||
if (patch != null) {
|
||||
val highlightAnnotation = fragment
|
||||
.document
|
||||
?.annotationProvider
|
||||
?.createAnnotationFromInstantJson(patch)
|
||||
|
||||
highlightAnnotation?.let {
|
||||
fragment.addAnnotationToPage(highlightAnnotation, true)
|
||||
highlightAnnotation?.let {
|
||||
fragment.addAnnotationToPage(highlightAnnotation, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,8 +133,8 @@ class PDFReaderViewModel @Inject constructor(
|
|||
annotation = Optional.presentIfNotNull(note),
|
||||
articleId = itemID,
|
||||
id = highlightID,
|
||||
patch = newAnnotation.toInstantJson(),
|
||||
quote = quote,
|
||||
patch = Optional.presentIfNotNull(newAnnotation.toInstantJson()),
|
||||
quote = Optional.presentIfNotNull(quote),
|
||||
shortId = shortID,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
|
|
@ -32,6 +33,8 @@ import androidx.core.view.ViewCompat
|
|||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import app.omnivore.omnivore.MainActivity
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.components.WebReaderLabelsSelectionSheet
|
||||
|
|
@ -40,6 +43,7 @@ import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
|||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlin.math.roundToInt
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
|
@ -51,6 +55,7 @@ class WebReaderLoadingContainerActivity: ComponentActivity() {
|
|||
val requestID = intent.getStringExtra("SAVED_ITEM_REQUEST_ID")
|
||||
val slug = intent.getStringExtra("SAVED_ITEM_SLUG")
|
||||
|
||||
|
||||
setContent {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val useDarkIcons = !isSystemInDarkTheme()
|
||||
|
|
@ -77,7 +82,7 @@ class WebReaderLoadingContainerActivity: ComponentActivity() {
|
|||
requestID = requestID,
|
||||
slug = slug,
|
||||
onLibraryIconTap = if (requestID != null) { { startMainActivity() } } else null,
|
||||
webReaderViewModel = viewModel
|
||||
webReaderViewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +142,17 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, o
|
|||
}),
|
||||
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (onLibraryIconTap != null) {
|
||||
IconButton(onClick = { onLibraryIconTap() }) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import app.omnivore.omnivore.networking.*
|
|||
import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
|
|
@ -394,7 +396,8 @@ class WebReaderViewModel @Inject constructor(
|
|||
fun createNewSavedItemLabel(labelName: String, hexColorValue: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newLabel = networker.createNewLabel(CreateLabelInput(color = hexColorValue, name = labelName))
|
||||
|
||||
val newLabel = networker.createNewLabel(CreateLabelInput(color = Optional.presentIfNotNull(hexColorValue), name = labelName))
|
||||
|
||||
newLabel?.let {
|
||||
val savedItemLabel = SavedItemLabel(
|
||||
|
|
|
|||
|
|
@ -13,11 +13,15 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.dataService.DataService
|
||||
import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.ui.auth.WelcomeScreen
|
||||
import app.omnivore.omnivore.ui.library.LibraryView
|
||||
import app.omnivore.omnivore.ui.library.SearchView
|
||||
import app.omnivore.omnivore.ui.library.LibraryViewModel
|
||||
import app.omnivore.omnivore.ui.library.SearchViewModel
|
||||
import app.omnivore.omnivore.ui.settings.PolicyWebView
|
||||
import app.omnivore.omnivore.ui.settings.SettingsViewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
|
@ -25,6 +29,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|||
@Composable
|
||||
fun RootView(
|
||||
loginViewModel: LoginViewModel,
|
||||
searchViewModel: SearchViewModel,
|
||||
libraryViewModel: LibraryViewModel,
|
||||
settingsViewModel: SettingsViewModel
|
||||
) {
|
||||
|
|
@ -48,6 +53,7 @@ fun RootView(
|
|||
if (hasAuthToken) {
|
||||
PrimaryNavigator(
|
||||
loginViewModel = loginViewModel,
|
||||
searchViewModel = searchViewModel,
|
||||
libraryViewModel = libraryViewModel,
|
||||
settingsViewModel = settingsViewModel
|
||||
)
|
||||
|
|
@ -68,6 +74,7 @@ fun RootView(
|
|||
fun PrimaryNavigator(
|
||||
loginViewModel: LoginViewModel,
|
||||
libraryViewModel: LibraryViewModel,
|
||||
searchViewModel: SearchViewModel,
|
||||
settingsViewModel: SettingsViewModel
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
|
@ -80,6 +87,13 @@ fun PrimaryNavigator(
|
|||
)
|
||||
}
|
||||
|
||||
composable(Routes.Search.route) {
|
||||
SearchView(
|
||||
viewModel = searchViewModel,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.Settings.route) {
|
||||
SettingsView(loginViewModel = loginViewModel, settingsViewModel = settingsViewModel, navController = navController)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,82 +9,92 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.SuggestionChipDefaults.elevatedSuggestionChipColors
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.ui.components.LabelChipColors
|
||||
import app.omnivore.omnivore.ui.library.LibraryViewModel
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import app.omnivore.omnivore.ui.library.SavedItemViewModel
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
|
||||
)
|
||||
@Composable
|
||||
fun SavedItemCard(cardData: SavedItemCardData, labels: List<SavedItemLabel>, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
val publisherDisplayName = cardData.publisherDisplayName()
|
||||
fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWithLabelsAndHighlights, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val actionsMenuItem: SavedItemWithLabelsAndHighlights? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClickHandler,
|
||||
onLongClick = { isMenuExpanded = true }
|
||||
onLongClick = { savedItemViewModel.actionsMenuItemLiveData.postValue(savedItem) }
|
||||
)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
.background(if (isMenuExpanded) Color.LightGray else Color.Transparent)
|
||||
.padding(10.dp)
|
||||
.background(Color.Transparent)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.padding(end = 8.dp)
|
||||
.padding(end = 20.dp)
|
||||
.defaultMinSize(minHeight = 55.dp)
|
||||
) {
|
||||
readInfo(item = savedItem)
|
||||
|
||||
Text(
|
||||
text = cardData.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
text = savedItem.savedItem.title,
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
),
|
||||
maxLines = 2,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
if (cardData.author != null && cardData.author != "") {
|
||||
if (savedItem.savedItem.author != null && savedItem.savedItem.author != "") {
|
||||
Text(
|
||||
text = "By ${cardData.author}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
if (publisherDisplayName != null) {
|
||||
Text(
|
||||
text = publisherDisplayName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = byline(savedItem),
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (cardData.imageURLString != null) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(cardData.imageURLString),
|
||||
contentDescription = "Image associated with saved item",
|
||||
modifier = Modifier
|
||||
.padding(top = 6.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.size(80.dp)
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(savedItem.savedItem.imageURLString),
|
||||
contentDescription = "Image associated with saved item",
|
||||
modifier = Modifier
|
||||
.size(55.dp, 73.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.defaultMinSize(minWidth = 55.dp, minHeight = 73.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
)
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
|
|
@ -92,9 +102,9 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List<SavedItemLabel>, onC
|
|||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(start = 6.dp)
|
||||
.padding(start = 10.dp, bottom = 5.dp, end = 10.dp)
|
||||
) {
|
||||
items(labels.sortedBy { it.name }) { label ->
|
||||
items(savedItem.labels.sortedBy { it.name }) { label ->
|
||||
val chipColors = LabelChipColors.fromHex(label.color)
|
||||
|
||||
SuggestionChip(
|
||||
|
|
@ -106,19 +116,121 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List<SavedItemLabel>, onC
|
|||
labelColor = chipColors.textColor,
|
||||
iconContentColor = chipColors.textColor
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
modifier = Modifier.padding(end = 5.dp)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp)
|
||||
|
||||
SavedItemContextMenu(
|
||||
isExpanded = isMenuExpanded,
|
||||
isArchived = cardData.isArchived,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
actionHandler = actionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun byline(item: SavedItemWithLabelsAndHighlights): String {
|
||||
item.savedItem.author?.let {
|
||||
return item.savedItem.author
|
||||
}
|
||||
|
||||
val publisherDisplayName = item.savedItem.publisherDisplayName()
|
||||
publisherDisplayName?.let {
|
||||
return publisherDisplayName
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
//
|
||||
//var readingSpeed: Int64 {
|
||||
// var result = UserDefaults.standard.integer(forKey: UserDefaultKey.userWordsPerMinute.rawValue)
|
||||
// if result <= 0 {
|
||||
// result = 235
|
||||
// }
|
||||
// return Int64(result)
|
||||
//}
|
||||
|
||||
fun estimatedReadingTime(item: SavedItemWithLabelsAndHighlights): String {
|
||||
item.savedItem.wordsCount?.let {
|
||||
if (it > 0) {
|
||||
val readLen = Math.max(1, it / 235)
|
||||
return "$readLen MIN READ • "
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fun readingProgress(item: SavedItemWithLabelsAndHighlights): String {
|
||||
// If there is no wordsCount don't show progress because it will make no sense
|
||||
item.savedItem.wordsCount?.let {
|
||||
if (it > 0) {
|
||||
val intVal = item.savedItem.readingProgress.toInt()
|
||||
return "$intVal%"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
//var highlightsText: String {
|
||||
// item.hig ?.let {
|
||||
// if let highlights = item.highlights, highlights.count > 0 {
|
||||
// let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count)
|
||||
// if item.wordsCount > 0 {
|
||||
// return " • \(fmted)"
|
||||
// }
|
||||
// return fmted
|
||||
// }
|
||||
// return ""
|
||||
//}
|
||||
//
|
||||
//var notesText: String {
|
||||
// let notes = item.highlights?.filter { item in
|
||||
// if let highlight = item as? Highlight {
|
||||
// return !(highlight.annotation ?? "").isEmpty
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// if let notes = notes, notes.count > 0 {
|
||||
// let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count)
|
||||
// if item.wordsCount > 0 {
|
||||
// return " • \(fmted)"
|
||||
// }
|
||||
// return fmted
|
||||
// }
|
||||
// return ""
|
||||
//}
|
||||
|
||||
@Composable
|
||||
fun readInfo(item: SavedItemWithLabelsAndHighlights) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 15.dp)
|
||||
) {
|
||||
Text(
|
||||
text = estimatedReadingTime(item),
|
||||
style = TextStyle(
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = readingProgress(item),
|
||||
style = TextStyle(
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (item.savedItem.readingProgress > 1) colorResource(R.color.green_55B938) else colorResource(R.color.gray_898989)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
// Text("\(highlightsText)")
|
||||
//
|
||||
// Text("\(notesText)")
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package app.omnivore.omnivore.ui.savedItemViews
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.SuggestionChipDefaults.elevatedSuggestionChipColors
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.TypeaheadCardData
|
||||
import app.omnivore.omnivore.ui.components.LabelChipColors
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
|
||||
)
|
||||
@Composable
|
||||
fun TypeaheadSearchCard(cardData: TypeaheadCardData, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClickHandler,
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(15.dp)
|
||||
.background(Color.Transparent)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.padding(end = 20.dp)
|
||||
.defaultMinSize(minHeight = 55.dp)
|
||||
) {
|
||||
Text(
|
||||
text = cardData.title,
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
),
|
||||
maxLines = 2,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<path
|
||||
android:pathData="M200,128a8,8 0,0 1,-8 8L64,136a8,8 0,0 1,0 -16L192,120A8,8 0,0 1,200 128ZM232,72L24,72a8,8 0,0 0,0 16L232,88a8,8 0,0 0,0 -16ZM152,168L104,168a8,8 0,0 0,0 16h48a8,8 0,0 0,0 -16Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
9
android/Omnivore/app/src/main/res/drawable/unarchive.xml
Normal file
9
android/Omnivore/app/src/main/res/drawable/unarchive.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<path
|
||||
android:pathData="M208,32L48,32A16,16 0,0 0,32 48L32,208a16,16 0,0 0,16 16L208,224a16,16 0,0 0,16 -16L224,48A16,16 0,0 0,208 32ZM208,48L208,152h-28.7A15.86,15.86 0,0 0,168 156.69L148.69,176L107.31,176L88,156.69A15.86,15.86 0,0 0,76.69 152L48,152L48,48ZM208,208L48,208L48,168L76.69,168L96,187.31A15.86,15.86 0,0 0,107.31 192h41.38A15.86,15.86 0,0 0,160 187.31L179.31,168L208,168v40ZM90.34,125.66a8,8 0,0 1,11.32 -11.32L120,132.69L120,72a8,8 0,0 1,16 0v60.69l18.34,-18.35a8,8 0,0 1,11.32 11.32l-32,32a8,8 0,0 1,-11.32 0Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
|
|
@ -4,4 +4,10 @@
|
|||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="cloud">#F7F7F7</color>
|
||||
<color name="gray_B7B7B7">#B7B7B7</color>
|
||||
|
||||
<color name="gray_898989">#898989</color>
|
||||
<color name="green_55B938">#55B938</color>
|
||||
<color name="gray_EBEBEB">#EBEBEB</color>
|
||||
|
||||
<color name="yellow_E6E4BF">#E6E4BF</color>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Reference in a new issue