diff --git a/android/SaveToOmnivore/.gitignore b/android/SaveToOmnivore/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/android/SaveToOmnivore/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/android/SaveToOmnivore/app/.gitignore b/android/SaveToOmnivore/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/android/SaveToOmnivore/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/SaveToOmnivore/app/build.gradle b/android/SaveToOmnivore/app/build.gradle new file mode 100644 index 000000000..9c89431b1 --- /dev/null +++ b/android/SaveToOmnivore/app/build.gradle @@ -0,0 +1,73 @@ + +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id("com.apollographql.apollo3").version("3.4.0") +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "app.omnivore.savetoomnivore" + minSdk 23 + targetSdk 32 + versionCode 3 + versionName "0.3" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion "1.2.0-alpha08" + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation "androidx.compose.ui:ui:$compose_version" + implementation 'androidx.compose.material3:material3:1.0.0-alpha14' + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.activity:activity-compose:1.3.1' + implementation 'com.apollographql.apollo3:apollo-runtime:3.4.0' + implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha03' + implementation 'androidx.datastore:datastore-core:1.0.0-rc01' + implementation "androidx.datastore:datastore-preferences:1.0.0" + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" +} + +apollo { + packageName.set("app.omnivore.generated") +} \ No newline at end of file diff --git a/android/SaveToOmnivore/app/proguard-rules.pro b/android/SaveToOmnivore/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/android/SaveToOmnivore/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/androidTest/java/app/omnivore/savetoomnivore/ExampleInstrumentedTest.kt b/android/SaveToOmnivore/app/src/androidTest/java/app/omnivore/savetoomnivore/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..7bda9bc5a --- /dev/null +++ b/android/SaveToOmnivore/app/src/androidTest/java/app/omnivore/savetoomnivore/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package app.omnivore.savetoomnivore + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.omnivore.savetoomnivore", appContext.packageName) + } +} \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/AndroidManifest.xml b/android/SaveToOmnivore/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..bcab33f06 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/graphql/SaveUrl.graphql b/android/SaveToOmnivore/app/src/main/graphql/SaveUrl.graphql new file mode 100644 index 000000000..c2a2d2428 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/graphql/SaveUrl.graphql @@ -0,0 +1,10 @@ +mutation SaveUrl ($input: SaveUrlInput!) { + saveUrl(input:$input){ + ... on SaveSuccess { + url + } + ... on SaveError { + errorCodes + } + } +} \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/graphql/schema.graphqls b/android/SaveToOmnivore/app/src/main/graphql/schema.graphqls new file mode 100644 index 000000000..187b8d072 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/graphql/schema.graphqls @@ -0,0 +1,1829 @@ +directive @sanitize(allowedTags: [String], maxLength: Int, pattern: String) on INPUT_FIELD_DEFINITION + +type AddPopularReadError { + errorCodes: [AddPopularReadErrorCode!]! +} + +enum AddPopularReadErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union AddPopularReadResult = AddPopularReadError | AddPopularReadSuccess + +type AddPopularReadSuccess { + pageId: String! +} + +type ApiKey { + createdAt: Date! + expiresAt: Date! + id: ID! + key: String + name: String! + scopes: [String!] + usedAt: Date +} + +type ApiKeysError { + errorCodes: [ApiKeysErrorCode!]! +} + +enum ApiKeysErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union ApiKeysResult = ApiKeysError | ApiKeysSuccess + +type ApiKeysSuccess { + apiKeys: [ApiKey!]! +} + +type ArchiveLinkError { + errorCodes: [ArchiveLinkErrorCode!]! + message: String! +} + +enum ArchiveLinkErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +input ArchiveLinkInput { + archived: Boolean! + linkId: ID! +} + +union ArchiveLinkResult = ArchiveLinkError | ArchiveLinkSuccess + +type ArchiveLinkSuccess { + linkId: String! + message: String! +} + +type Article { + author: String + content: String! + contentReader: ContentReader! + createdAt: Date! + description: String + hasContent: Boolean + hash: String! + highlights(input: ArticleHighlightsInput): [Highlight!]! + id: ID! + image: String + isArchived: Boolean! + labels: [Label!] + language: String + linkId: ID + originalArticleUrl: String + originalHtml: String + pageType: PageType + postedByViewer: Boolean + publishedAt: Date + readAt: Date + readingProgressAnchorIndex: Int! + readingProgressPercent: Float! + savedAt: Date! + savedByViewer: Boolean + shareInfo: LinkShareInfo + sharedComment: String + siteIcon: String + siteName: String + slug: String! + state: ArticleSavingRequestStatus + subscription: String + title: String! + unsubHttpUrl: String + unsubMailTo: String + updatedAt: Date! + uploadFileId: ID + url: String! +} + +type ArticleEdge { + cursor: String! + node: Article! +} + +type ArticleError { + errorCodes: [ArticleErrorCode!]! +} + +enum ArticleErrorCode { + BAD_DATA + NOT_FOUND + UNAUTHORIZED +} + +input ArticleHighlightsInput { + includeFriends: Boolean +} + +union ArticleResult = ArticleError | ArticleSuccess + +type ArticleSavingRequest { + article: Article @deprecated(reason: "article has been replaced with slug") + createdAt: Date! + errorCode: CreateArticleErrorCode + id: ID! + slug: String! + status: ArticleSavingRequestStatus! + updatedAt: Date! + user: User! + userId: ID! @deprecated(reason: "userId has been replaced with user") +} + +type ArticleSavingRequestError { + errorCodes: [ArticleSavingRequestErrorCode!]! +} + +enum ArticleSavingRequestErrorCode { + NOT_FOUND + UNAUTHORIZED +} + +union ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequestSuccess + +enum ArticleSavingRequestStatus { + FAILED + PROCESSING + SUCCEEDED +} + +type ArticleSavingRequestSuccess { + articleSavingRequest: ArticleSavingRequest! +} + +type ArticleSuccess { + article: Article! +} + +type ArticlesError { + errorCodes: [ArticlesErrorCode!]! +} + +enum ArticlesErrorCode { + UNAUTHORIZED +} + +union ArticlesResult = ArticlesError | ArticlesSuccess + +type ArticlesSuccess { + edges: [ArticleEdge!]! + pageInfo: PageInfo! +} + +enum ContentReader { + PDF + WEB +} + +type CreateArticleError { + errorCodes: [CreateArticleErrorCode!]! +} + +enum CreateArticleErrorCode { + ELASTIC_ERROR + NOT_ALLOWED_TO_PARSE + PAYLOAD_TOO_LARGE + UNABLE_TO_FETCH + UNABLE_TO_PARSE + UNAUTHORIZED + UPLOAD_FILE_MISSING +} + +input CreateArticleInput { + articleSavingRequestId: ID + preparedDocument: PreparedDocumentInput + skipParsing: Boolean + source: String + uploadFileId: ID + url: String! +} + +union CreateArticleResult = CreateArticleError | CreateArticleSuccess + +type CreateArticleSavingRequestError { + errorCodes: [CreateArticleSavingRequestErrorCode!]! +} + +enum CreateArticleSavingRequestErrorCode { + BAD_DATA + UNAUTHORIZED +} + +input CreateArticleSavingRequestInput { + url: String! +} + +union CreateArticleSavingRequestResult = CreateArticleSavingRequestError | CreateArticleSavingRequestSuccess + +type CreateArticleSavingRequestSuccess { + articleSavingRequest: ArticleSavingRequest! +} + +type CreateArticleSuccess { + created: Boolean! + createdArticle: Article! + user: User! +} + +type CreateHighlightError { + errorCodes: [CreateHighlightErrorCode!]! +} + +enum CreateHighlightErrorCode { + ALREADY_EXISTS + BAD_DATA + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +input CreateHighlightInput { + annotation: String + articleId: ID! + id: ID! + patch: String! + prefix: String + quote: String! + sharedAt: Date + shortId: String! + suffix: String +} + +type CreateHighlightReplyError { + errorCodes: [CreateHighlightReplyErrorCode!]! +} + +enum CreateHighlightReplyErrorCode { + EMPTY_ANNOTATION + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +input CreateHighlightReplyInput { + highlightId: ID! + text: String! +} + +union CreateHighlightReplyResult = CreateHighlightReplyError | CreateHighlightReplySuccess + +type CreateHighlightReplySuccess { + highlightReply: HighlightReply! +} + +union CreateHighlightResult = CreateHighlightError | CreateHighlightSuccess + +type CreateHighlightSuccess { + highlight: Highlight! +} + +type CreateLabelError { + errorCodes: [CreateLabelErrorCode!]! +} + +enum CreateLabelErrorCode { + BAD_REQUEST + LABEL_ALREADY_EXISTS + NOT_FOUND + UNAUTHORIZED +} + +input CreateLabelInput { + color: String! + description: String + name: String! +} + +union CreateLabelResult = CreateLabelError | CreateLabelSuccess + +type CreateLabelSuccess { + label: Label! +} + +type CreateNewsletterEmailError { + errorCodes: [CreateNewsletterEmailErrorCode!]! +} + +enum CreateNewsletterEmailErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union CreateNewsletterEmailResult = CreateNewsletterEmailError | CreateNewsletterEmailSuccess + +type CreateNewsletterEmailSuccess { + newsletterEmail: NewsletterEmail! +} + +type CreateReactionError { + errorCodes: [CreateReactionErrorCode!]! +} + +enum CreateReactionErrorCode { + BAD_CODE + BAD_TARGET + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +input CreateReactionInput { + code: ReactionType! + highlightId: ID + userArticleId: ID +} + +union CreateReactionResult = CreateReactionError | CreateReactionSuccess + +type CreateReactionSuccess { + reaction: Reaction! +} + +type CreateReminderError { + errorCodes: [CreateReminderErrorCode!]! +} + +enum CreateReminderErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input CreateReminderInput { + archiveUntil: Boolean! + clientRequestId: ID + linkId: ID + remindAt: Date! + sendNotification: Boolean! +} + +union CreateReminderResult = CreateReminderError | CreateReminderSuccess + +type CreateReminderSuccess { + reminder: Reminder! +} + +scalar Date + +type DeleteAccountError { + errorCodes: [DeleteAccountErrorCode!]! +} + +enum DeleteAccountErrorCode { + FORBIDDEN + UNAUTHORIZED + USER_NOT_FOUND +} + +union DeleteAccountResult = DeleteAccountError | DeleteAccountSuccess + +type DeleteAccountSuccess { + userID: ID! +} + +type DeleteHighlightError { + errorCodes: [DeleteHighlightErrorCode!]! +} + +enum DeleteHighlightErrorCode { + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +type DeleteHighlightReplyError { + errorCodes: [DeleteHighlightReplyErrorCode!]! +} + +enum DeleteHighlightReplyErrorCode { + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +union DeleteHighlightReplyResult = DeleteHighlightReplyError | DeleteHighlightReplySuccess + +type DeleteHighlightReplySuccess { + highlightReply: HighlightReply! +} + +union DeleteHighlightResult = DeleteHighlightError | DeleteHighlightSuccess + +type DeleteHighlightSuccess { + highlight: Highlight! +} + +type DeleteLabelError { + errorCodes: [DeleteLabelErrorCode!]! +} + +enum DeleteLabelErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union DeleteLabelResult = DeleteLabelError | DeleteLabelSuccess + +type DeleteLabelSuccess { + label: Label! +} + +type DeleteNewsletterEmailError { + errorCodes: [DeleteNewsletterEmailErrorCode!]! +} + +enum DeleteNewsletterEmailErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union DeleteNewsletterEmailResult = DeleteNewsletterEmailError | DeleteNewsletterEmailSuccess + +type DeleteNewsletterEmailSuccess { + newsletterEmail: NewsletterEmail! +} + +type DeleteReactionError { + errorCodes: [DeleteReactionErrorCode!]! +} + +enum DeleteReactionErrorCode { + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +union DeleteReactionResult = DeleteReactionError | DeleteReactionSuccess + +type DeleteReactionSuccess { + reaction: Reaction! +} + +type DeleteReminderError { + errorCodes: [DeleteReminderErrorCode!]! +} + +enum DeleteReminderErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union DeleteReminderResult = DeleteReminderError | DeleteReminderSuccess + +type DeleteReminderSuccess { + reminder: Reminder! +} + +type DeleteWebhookError { + errorCodes: [DeleteWebhookErrorCode!]! +} + +enum DeleteWebhookErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union DeleteWebhookResult = DeleteWebhookError | DeleteWebhookSuccess + +type DeleteWebhookSuccess { + webhook: Webhook! +} + +type DeviceToken { + createdAt: Date! + id: ID! + token: String! +} + +type FeedArticle { + annotationsCount: Int + article: Article! + highlight: Highlight + highlightsCount: Int + id: ID! + reactions: [Reaction!]! + sharedAt: Date! + sharedBy: User! + sharedComment: String + sharedWithHighlights: Boolean +} + +type FeedArticleEdge { + cursor: String! + node: FeedArticle! +} + +type FeedArticlesError { + errorCodes: [FeedArticlesErrorCode!]! +} + +enum FeedArticlesErrorCode { + UNAUTHORIZED +} + +union FeedArticlesResult = FeedArticlesError | FeedArticlesSuccess + +type FeedArticlesSuccess { + edges: [FeedArticleEdge!]! + pageInfo: PageInfo! +} + +type GenerateApiKeyError { + errorCodes: [GenerateApiKeyErrorCode!]! +} + +enum GenerateApiKeyErrorCode { + ALREADY_EXISTS + BAD_REQUEST + UNAUTHORIZED +} + +input GenerateApiKeyInput { + expiresAt: Date! + name: String! + scopes: [String!] +} + +union GenerateApiKeyResult = GenerateApiKeyError | GenerateApiKeySuccess + +type GenerateApiKeySuccess { + apiKey: ApiKey! +} + +type GetFollowersError { + errorCodes: [GetFollowersErrorCode!]! +} + +enum GetFollowersErrorCode { + UNAUTHORIZED +} + +union GetFollowersResult = GetFollowersError | GetFollowersSuccess + +type GetFollowersSuccess { + followers: [User!]! +} + +type GetFollowingError { + errorCodes: [GetFollowingErrorCode!]! +} + +enum GetFollowingErrorCode { + UNAUTHORIZED +} + +union GetFollowingResult = GetFollowingError | GetFollowingSuccess + +type GetFollowingSuccess { + following: [User!]! +} + +type GetUserPersonalizationError { + errorCodes: [GetUserPersonalizationErrorCode!]! +} + +enum GetUserPersonalizationErrorCode { + UNAUTHORIZED +} + +union GetUserPersonalizationResult = GetUserPersonalizationError | GetUserPersonalizationSuccess + +type GetUserPersonalizationSuccess { + userPersonalization: UserPersonalization +} + +input GoogleLoginInput { + email: String! + secret: String! +} + +type GoogleSignupError { + errorCodes: [SignupErrorCode]! +} + +input GoogleSignupInput { + bio: String + email: String! + name: String! + pictureUrl: String! + secret: String! + sourceUserId: String! + username: String! +} + +union GoogleSignupResult = GoogleSignupError | GoogleSignupSuccess + +type GoogleSignupSuccess { + me: User! +} + +type Highlight { + annotation: String + createdAt: Date! + createdByMe: Boolean! + id: ID! + patch: String! + prefix: String + quote: String! + reactions: [Reaction!]! + replies: [HighlightReply!]! + sharedAt: Date + shortId: String! + suffix: String + updatedAt: Date! + user: User! +} + +type HighlightReply { + createdAt: Date! + highlight: Highlight! + id: ID! + text: String! + updatedAt: Date! + user: User! +} + +type HighlightStats { + highlightCount: Int! +} + +type Label { + color: String! + createdAt: Date + description: String + id: ID! + name: String! +} + +type LabelsError { + errorCodes: [LabelsErrorCode!]! +} + +enum LabelsErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union LabelsResult = LabelsError | LabelsSuccess + +type LabelsSuccess { + labels: [Label!]! +} + +type Link { + highlightStats: HighlightStats! + id: ID! + page: Page! + postedByViewer: Boolean! + readState: ReadState! + savedAt: Date! + savedBy: User! + savedByViewer: Boolean! + shareInfo: LinkShareInfo! + shareStats: ShareStats! + slug: String! + updatedAt: Date! + url: String! +} + +type LinkShareInfo { + description: String! + imageUrl: String! + title: String! +} + +type LogOutError { + errorCodes: [LogOutErrorCode!]! +} + +enum LogOutErrorCode { + LOG_OUT_FAILED +} + +union LogOutResult = LogOutError | LogOutSuccess + +type LogOutSuccess { + message: String +} + +type LoginError { + errorCodes: [LoginErrorCode!]! +} + +enum LoginErrorCode { + ACCESS_DENIED + AUTH_FAILED + INVALID_CREDENTIALS + USER_ALREADY_EXISTS + USER_NOT_FOUND + WRONG_SOURCE +} + +input LoginInput { + email: String! + password: String! +} + +union LoginResult = LoginError | LoginSuccess + +type LoginSuccess { + me: User! +} + +type MergeHighlightError { + errorCodes: [MergeHighlightErrorCode!]! +} + +enum MergeHighlightErrorCode { + ALREADY_EXISTS + BAD_DATA + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +input MergeHighlightInput { + annotation: String + articleId: ID! + id: ID! + overlapHighlightIdList: [String!]! + patch: String! + prefix: String + quote: String! + shortId: ID! + suffix: String +} + +union MergeHighlightResult = MergeHighlightError | MergeHighlightSuccess + +type MergeHighlightSuccess { + highlight: Highlight! + overlapHighlightIdList: [String!]! +} + +type Mutation { + addPopularRead(name: String!): AddPopularReadResult! + createArticle(input: CreateArticleInput!): CreateArticleResult! + createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult! + createHighlight(input: CreateHighlightInput!): CreateHighlightResult! + createHighlightReply(input: CreateHighlightReplyInput!): CreateHighlightReplyResult! + createLabel(input: CreateLabelInput!): CreateLabelResult! + createNewsletterEmail: CreateNewsletterEmailResult! + createReaction(input: CreateReactionInput!): CreateReactionResult! + createReminder(input: CreateReminderInput!): CreateReminderResult! + deleteAccount(userID: ID!): DeleteAccountResult! + deleteHighlight(highlightId: ID!): DeleteHighlightResult! + deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult! + deleteLabel(id: ID!): DeleteLabelResult! + deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult! + deleteReaction(id: ID!): DeleteReactionResult! + deleteReminder(id: ID!): DeleteReminderResult! + deleteWebhook(id: ID!): DeleteWebhookResult! + generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult! + googleLogin(input: GoogleLoginInput!): LoginResult! + googleSignup(input: GoogleSignupInput!): GoogleSignupResult! + logOut: LogOutResult! + login(input: LoginInput!): LoginResult! + mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult! + reportItem(input: ReportItemInput!): ReportItemResult! + revokeApiKey(id: ID!): RevokeApiKeyResult! + saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult! + saveFile(input: SaveFileInput!): SaveResult! + savePage(input: SavePageInput!): SaveResult! + saveUrl(input: SaveUrlInput!): SaveResult! + setBookmarkArticle(input: SetBookmarkArticleInput!): SetBookmarkArticleResult! + setDeviceToken(input: SetDeviceTokenInput!): SetDeviceTokenResult! + setFollow(input: SetFollowInput!): SetFollowResult! + setLabels(input: SetLabelsInput!): SetLabelsResult! + setLabelsForHighlight(input: SetLabelsForHighlightInput!): SetLabelsResult! + setLinkArchived(input: ArchiveLinkInput!): ArchiveLinkResult! + setShareArticle(input: SetShareArticleInput!): SetShareArticleResult! + setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult! + setUserPersonalization(input: SetUserPersonalizationInput!): SetUserPersonalizationResult! + setWebhook(input: SetWebhookInput!): SetWebhookResult! + signup(input: SignupInput!): SignupResult! + subscribe(name: String!): SubscribeResult! + unsubscribe(name: String!): UnsubscribeResult! + updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult! + updateHighlightReply(input: UpdateHighlightReplyInput!): UpdateHighlightReplyResult! + updateLabel(input: UpdateLabelInput!): UpdateLabelResult! + updateLinkShareInfo(input: UpdateLinkShareInfoInput!): UpdateLinkShareInfoResult! + updatePage(input: UpdatePageInput!): UpdatePageResult! + updateReminder(input: UpdateReminderInput!): UpdateReminderResult! + updateSharedComment(input: UpdateSharedCommentInput!): UpdateSharedCommentResult! + updateUser(input: UpdateUserInput!): UpdateUserResult! + updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResult! + uploadFileRequest(input: UploadFileRequestInput!): UploadFileRequestResult! +} + +type NewsletterEmail { + address: String! + confirmationCode: String + id: ID! +} + +type NewsletterEmailsError { + errorCodes: [NewsletterEmailsErrorCode!]! +} + +enum NewsletterEmailsErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union NewsletterEmailsResult = NewsletterEmailsError | NewsletterEmailsSuccess + +type NewsletterEmailsSuccess { + newsletterEmails: [NewsletterEmail!]! +} + +type Page { + author: String + createdAt: Date! + description: String + hash: String! + id: ID! + image: String! + originalHtml: String! + originalUrl: String! + publishedAt: Date + readableHtml: String! + title: String! + type: PageType! + updatedAt: Date! + url: String! +} + +type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + totalCount: Int +} + +input PageInfoInput { + author: String + canonicalUrl: String + contentType: String + description: String + previewImage: String + publishedAt: Date + title: String +} + +enum PageType { + ARTICLE + BOOK + FILE + HIGHLIGHTS + PROFILE + UNKNOWN + WEBSITE +} + +input PreparedDocumentInput { + document: String! + pageInfo: PageInfoInput! +} + +type Profile { + bio: String + id: ID! + pictureUrl: String + private: Boolean! + username: String! +} + +type Query { + apiKeys: ApiKeysResult! + article(slug: String!, username: String!): ArticleResult! + articleSavingRequest(id: ID!): ArticleSavingRequestResult! + articles(after: String, first: Int, includePending: Boolean, query: String, sharedOnly: Boolean, sort: SortParams): ArticlesResult! + feedArticles(after: String, first: Int, sharedByUser: ID, sort: SortParams): FeedArticlesResult! + getFollowers(userId: ID): GetFollowersResult! + getFollowing(userId: ID): GetFollowingResult! + getUserPersonalization: GetUserPersonalizationResult! + hello: String + labels: LabelsResult! + me: User + newsletterEmails: NewsletterEmailsResult! + reminder(linkId: ID!): ReminderResult! + search(after: String, first: Int, query: String): SearchResult! + sendInstallInstructions: SendInstallInstructionsResult! + sharedArticle(selectedHighlightId: String, slug: String!, username: String!): SharedArticleResult! + subscriptions(sort: SortParams): SubscriptionsResult! + typeaheadSearch(first: Int, query: String!): TypeaheadSearchResult! + user(userId: ID, username: String): UserResult! + users: UsersResult! + validateUsername(username: String!): Boolean! + webhook(id: ID!): WebhookResult! + webhooks: WebhooksResult! +} + +type Reaction { + code: ReactionType! + createdAt: Date! + id: ID! + updatedAt: Date + user: User! +} + +enum ReactionType { + CRYING + HEART + HUSHED + LIKE + POUT + SMILE +} + +type ReadState { + progressAnchorIndex: Int! + progressPercent: Float! + reading: Boolean + readingTime: Int +} + +type Reminder { + archiveUntil: Boolean! + id: ID! + remindAt: Date! + sendNotification: Boolean! +} + +type ReminderError { + errorCodes: [ReminderErrorCode!]! +} + +enum ReminderErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union ReminderResult = ReminderError | ReminderSuccess + +type ReminderSuccess { + reminder: Reminder! +} + +input ReportItemInput { + itemUrl: String! + pageId: ID! + reportComment: String! + reportTypes: [ReportType!]! + sharedBy: ID +} + +type ReportItemResult { + message: String! +} + +enum ReportType { + ABUSIVE + CONTENT_DISPLAY + CONTENT_VIOLATION + SPAM +} + +type RevokeApiKeyError { + errorCodes: [RevokeApiKeyErrorCode!]! +} + +enum RevokeApiKeyErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union RevokeApiKeyResult = RevokeApiKeyError | RevokeApiKeySuccess + +type RevokeApiKeySuccess { + apiKey: ApiKey! +} + +type SaveArticleReadingProgressError { + errorCodes: [SaveArticleReadingProgressErrorCode!]! +} + +enum SaveArticleReadingProgressErrorCode { + BAD_DATA + NOT_FOUND + UNAUTHORIZED +} + +input SaveArticleReadingProgressInput { + id: ID! + readingProgressAnchorIndex: Int! + readingProgressPercent: Float! +} + +union SaveArticleReadingProgressResult = SaveArticleReadingProgressError | SaveArticleReadingProgressSuccess + +type SaveArticleReadingProgressSuccess { + updatedArticle: Article! +} + +type SaveError { + errorCodes: [SaveErrorCode!]! + message: String +} + +enum SaveErrorCode { + UNAUTHORIZED + UNKNOWN +} + +input SaveFileInput { + clientRequestId: ID! + source: String! + uploadFileId: ID! + url: String! +} + +input SavePageInput { + clientRequestId: ID! + originalContent: String! + source: String! + title: String + url: String! +} + +union SaveResult = SaveError | SaveSuccess + +type SaveSuccess { + clientRequestId: ID! + url: String! +} + +input SaveUrlInput { + clientRequestId: ID! + source: String! + url: String! +} + +type SearchError { + errorCodes: [SearchErrorCode!]! +} + +enum SearchErrorCode { + UNAUTHORIZED +} + +type SearchItem { + annotation: String + author: String + contentReader: ContentReader! + createdAt: Date! + description: String + highlights: [Highlight!] + id: ID! + image: String + isArchived: Boolean! + labels: [Label!] + language: String + originalArticleUrl: String + ownedByViewer: Boolean + pageId: ID + pageType: PageType! + publishedAt: Date + quote: String + readAt: Date + readingProgressAnchorIndex: Int! + readingProgressPercent: Float! + savedAt: Date! + shortId: String + siteName: String + slug: String! + state: ArticleSavingRequestStatus + subscription: String + title: String! + unsubHttpUrl: String + unsubMailTo: String + updatedAt: Date + uploadFileId: ID + url: String! +} + +type SearchItemEdge { + cursor: String! + node: SearchItem! +} + +union SearchResult = SearchError | SearchSuccess + +type SearchSuccess { + edges: [SearchItemEdge!]! + pageInfo: PageInfo! +} + +type SendInstallInstructionsError { + errorCodes: [SendInstallInstructionsErrorCode!]! +} + +enum SendInstallInstructionsErrorCode { + BAD_REQUEST + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +union SendInstallInstructionsResult = SendInstallInstructionsError | SendInstallInstructionsSuccess + +type SendInstallInstructionsSuccess { + sent: Boolean! +} + +type SetBookmarkArticleError { + errorCodes: [SetBookmarkArticleErrorCode!]! +} + +enum SetBookmarkArticleErrorCode { + BOOKMARK_EXISTS + NOT_FOUND +} + +input SetBookmarkArticleInput { + articleID: ID! + bookmark: Boolean! +} + +union SetBookmarkArticleResult = SetBookmarkArticleError | SetBookmarkArticleSuccess + +type SetBookmarkArticleSuccess { + bookmarkedArticle: Article! +} + +type SetDeviceTokenError { + errorCodes: [SetDeviceTokenErrorCode!]! +} + +enum SetDeviceTokenErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input SetDeviceTokenInput { + id: ID + token: String +} + +union SetDeviceTokenResult = SetDeviceTokenError | SetDeviceTokenSuccess + +type SetDeviceTokenSuccess { + deviceToken: DeviceToken! +} + +type SetFollowError { + errorCodes: [SetFollowErrorCode!]! +} + +enum SetFollowErrorCode { + NOT_FOUND + UNAUTHORIZED +} + +input SetFollowInput { + follow: Boolean! + userId: ID! +} + +union SetFollowResult = SetFollowError | SetFollowSuccess + +type SetFollowSuccess { + updatedUser: User! +} + +type SetLabelsError { + errorCodes: [SetLabelsErrorCode!]! +} + +enum SetLabelsErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input SetLabelsForHighlightInput { + highlightId: ID! + labelIds: [ID!]! +} + +input SetLabelsInput { + labelIds: [ID!]! + pageId: ID! +} + +union SetLabelsResult = SetLabelsError | SetLabelsSuccess + +type SetLabelsSuccess { + labels: [Label!]! +} + +type SetShareArticleError { + errorCodes: [SetShareArticleErrorCode!]! +} + +enum SetShareArticleErrorCode { + NOT_FOUND + UNAUTHORIZED +} + +input SetShareArticleInput { + articleID: ID! + share: Boolean! + sharedComment: String + sharedWithHighlights: Boolean +} + +union SetShareArticleResult = SetShareArticleError | SetShareArticleSuccess + +type SetShareArticleSuccess { + updatedArticle: Article! + updatedFeedArticle: FeedArticle + updatedFeedArticleId: String +} + +type SetShareHighlightError { + errorCodes: [SetShareHighlightErrorCode!]! +} + +enum SetShareHighlightErrorCode { + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +input SetShareHighlightInput { + id: ID! + share: Boolean! +} + +union SetShareHighlightResult = SetShareHighlightError | SetShareHighlightSuccess + +type SetShareHighlightSuccess { + highlight: Highlight! +} + +type SetUserPersonalizationError { + errorCodes: [SetUserPersonalizationErrorCode!]! +} + +enum SetUserPersonalizationErrorCode { + UNAUTHORIZED +} + +input SetUserPersonalizationInput { + fontFamily: String + fontSize: Int + libraryLayoutType: String + librarySortOrder: SortOrder + margin: Int + theme: String +} + +union SetUserPersonalizationResult = SetUserPersonalizationError | SetUserPersonalizationSuccess + +type SetUserPersonalizationSuccess { + updatedUserPersonalization: UserPersonalization! +} + +type SetWebhookError { + errorCodes: [SetWebhookErrorCode!]! +} + +enum SetWebhookErrorCode { + ALREADY_EXISTS + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input SetWebhookInput { + contentType: String + enabled: Boolean + eventTypes: [WebhookEvent!]! + id: ID + method: String + url: String! +} + +union SetWebhookResult = SetWebhookError | SetWebhookSuccess + +type SetWebhookSuccess { + webhook: Webhook! +} + +type ShareStats { + readDuration: Int! + saveCount: Int! + viewCount: Int! +} + +type SharedArticleError { + errorCodes: [SharedArticleErrorCode!]! +} + +enum SharedArticleErrorCode { + NOT_FOUND +} + +union SharedArticleResult = SharedArticleError | SharedArticleSuccess + +type SharedArticleSuccess { + article: Article! +} + +type SignupError { + errorCodes: [SignupErrorCode]! +} + +enum SignupErrorCode { + ACCESS_DENIED + EXPIRED_TOKEN + GOOGLE_AUTH_ERROR + INVALID_PASSWORD + INVALID_USERNAME + UNKNOWN + USER_EXISTS +} + +input SignupInput { + bio: String + email: String! + name: String! + password: String! + pictureUrl: String + username: String! +} + +union SignupResult = SignupError | SignupSuccess + +type SignupSuccess { + me: User! +} + +enum SortBy { + PUBLISHED_AT + SAVED_AT + SCORE + UPDATED_TIME +} + +enum SortOrder { + ASCENDING + DESCENDING +} + +input SortParams { + by: SortBy! + order: SortOrder +} + +type SubscribeError { + errorCodes: [SubscribeErrorCode!]! +} + +enum SubscribeErrorCode { + ALREADY_SUBSCRIBED + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union SubscribeResult = SubscribeError | SubscribeSuccess + +type SubscribeSuccess { + subscriptions: [Subscription!]! +} + +type Subscription { + createdAt: Date! + description: String + id: ID! + name: String! + newsletterEmail: String! + status: SubscriptionStatus! + unsubscribeHttpUrl: String + unsubscribeMailTo: String + updatedAt: Date! + url: String +} + +enum SubscriptionStatus { + ACTIVE + DELETED + UNSUBSCRIBED +} + +type SubscriptionsError { + errorCodes: [SubscriptionsErrorCode!]! +} + +enum SubscriptionsErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union SubscriptionsResult = SubscriptionsError | SubscriptionsSuccess + +type SubscriptionsSuccess { + subscriptions: [Subscription!]! +} + +type TypeaheadSearchError { + errorCodes: [TypeaheadSearchErrorCode!]! +} + +enum TypeaheadSearchErrorCode { + UNAUTHORIZED +} + +type TypeaheadSearchItem { + id: ID! + siteName: String + slug: String! + title: String! +} + +union TypeaheadSearchResult = TypeaheadSearchError | TypeaheadSearchSuccess + +type TypeaheadSearchSuccess { + items: [TypeaheadSearchItem!]! +} + +type UnsubscribeError { + errorCodes: [UnsubscribeErrorCode!]! +} + +enum UnsubscribeErrorCode { + ALREADY_UNSUBSCRIBED + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED + UNSUBSCRIBE_METHOD_NOT_FOUND +} + +union UnsubscribeResult = UnsubscribeError | UnsubscribeSuccess + +type UnsubscribeSuccess { + subscription: Subscription! +} + +type UpdateHighlightError { + errorCodes: [UpdateHighlightErrorCode!]! +} + +enum UpdateHighlightErrorCode { + BAD_DATA + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +input UpdateHighlightInput { + annotation: String + highlightId: ID! + sharedAt: Date +} + +type UpdateHighlightReplyError { + errorCodes: [UpdateHighlightReplyErrorCode!]! +} + +enum UpdateHighlightReplyErrorCode { + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +input UpdateHighlightReplyInput { + highlightReplyId: ID! + text: String! +} + +union UpdateHighlightReplyResult = UpdateHighlightReplyError | UpdateHighlightReplySuccess + +type UpdateHighlightReplySuccess { + highlightReply: HighlightReply! +} + +union UpdateHighlightResult = UpdateHighlightError | UpdateHighlightSuccess + +type UpdateHighlightSuccess { + highlight: Highlight! +} + +type UpdateLabelError { + errorCodes: [UpdateLabelErrorCode!]! +} + +enum UpdateLabelErrorCode { + BAD_REQUEST + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +input UpdateLabelInput { + color: String! + description: String + labelId: ID! + name: String! +} + +union UpdateLabelResult = UpdateLabelError | UpdateLabelSuccess + +type UpdateLabelSuccess { + label: Label! +} + +type UpdateLinkShareInfoError { + errorCodes: [UpdateLinkShareInfoErrorCode!]! +} + +enum UpdateLinkShareInfoErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +input UpdateLinkShareInfoInput { + description: String! + linkId: ID! + title: String! +} + +union UpdateLinkShareInfoResult = UpdateLinkShareInfoError | UpdateLinkShareInfoSuccess + +type UpdateLinkShareInfoSuccess { + message: String! +} + +type UpdatePageError { + errorCodes: [UpdatePageErrorCode!]! +} + +enum UpdatePageErrorCode { + BAD_REQUEST + FORBIDDEN + NOT_FOUND + UNAUTHORIZED + UPDATE_FAILED +} + +input UpdatePageInput { + description: String + pageId: ID! + title: String +} + +union UpdatePageResult = UpdatePageError | UpdatePageSuccess + +type UpdatePageSuccess { + updatedPage: Article! +} + +type UpdateReminderError { + errorCodes: [UpdateReminderErrorCode!]! +} + +enum UpdateReminderErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input UpdateReminderInput { + archiveUntil: Boolean! + id: ID! + remindAt: Date! + sendNotification: Boolean! +} + +union UpdateReminderResult = UpdateReminderError | UpdateReminderSuccess + +type UpdateReminderSuccess { + reminder: Reminder! +} + +type UpdateSharedCommentError { + errorCodes: [UpdateSharedCommentErrorCode!]! +} + +enum UpdateSharedCommentErrorCode { + NOT_FOUND + UNAUTHORIZED +} + +input UpdateSharedCommentInput { + articleID: ID! + sharedComment: String! +} + +union UpdateSharedCommentResult = UpdateSharedCommentError | UpdateSharedCommentSuccess + +type UpdateSharedCommentSuccess { + articleID: ID! + sharedComment: String! +} + +type UpdateUserError { + errorCodes: [UpdateUserErrorCode!]! +} + +enum UpdateUserErrorCode { + BIO_TOO_LONG + EMPTY_NAME + UNAUTHORIZED + USER_NOT_FOUND +} + +input UpdateUserInput { + bio: String + name: String! +} + +type UpdateUserProfileError { + errorCodes: [UpdateUserProfileErrorCode!]! +} + +enum UpdateUserProfileErrorCode { + BAD_DATA + BAD_USERNAME + FORBIDDEN + UNAUTHORIZED + USERNAME_EXISTS +} + +input UpdateUserProfileInput { + bio: String + pictureUrl: String + userId: ID! + username: String +} + +union UpdateUserProfileResult = UpdateUserProfileError | UpdateUserProfileSuccess + +type UpdateUserProfileSuccess { + user: User! +} + +union UpdateUserResult = UpdateUserError | UpdateUserSuccess + +type UpdateUserSuccess { + user: User! +} + +type UploadFileRequestError { + errorCodes: [UploadFileRequestErrorCode!]! +} + +enum UploadFileRequestErrorCode { + BAD_INPUT + FAILED_CREATE + UNAUTHORIZED +} + +input UploadFileRequestInput { + clientRequestId: String + contentType: String! + createPageEntry: Boolean + url: String! +} + +union UploadFileRequestResult = UploadFileRequestError | UploadFileRequestSuccess + +type UploadFileRequestSuccess { + createdPageId: String + id: ID! + uploadFileId: ID + uploadSignedUrl: String +} + +enum UploadFileStatus { + COMPLETED + INITIALIZED +} + +type User { + followersCount: Int + friendsCount: Int + id: ID! + isFriend: Boolean @deprecated(reason: "isFriend has been replaced with viewerIsFollowing") + isFullUser: Boolean + name: String! + picture: String + profile: Profile! + sharedArticles: [FeedArticle!]! + sharedArticlesCount: Int + sharedHighlightsCount: Int + sharedNotesCount: Int + viewerIsFollowing: Boolean +} + +type UserError { + errorCodes: [UserErrorCode!]! +} + +enum UserErrorCode { + BAD_REQUEST + UNAUTHORIZED + USER_NOT_FOUND +} + +type UserPersonalization { + fontFamily: String + fontSize: Int + id: ID + libraryLayoutType: String + librarySortOrder: SortOrder + margin: Int + theme: String +} + +union UserResult = UserError | UserSuccess + +type UserSuccess { + user: User! +} + +type UsersError { + errorCodes: [UsersErrorCode!]! +} + +enum UsersErrorCode { + UNAUTHORIZED +} + +union UsersResult = UsersError | UsersSuccess + +type UsersSuccess { + users: [User!]! +} + +type Webhook { + contentType: String! + createdAt: Date! + enabled: Boolean! + eventTypes: [WebhookEvent!]! + id: ID! + method: String! + updatedAt: Date! + url: String! +} + +type WebhookError { + errorCodes: [WebhookErrorCode!]! +} + +enum WebhookErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +enum WebhookEvent { + HIGHLIGHT_CREATED + HIGHLIGHT_DELETED + HIGHLIGHT_UPDATED + LABEL_CREATED + LABEL_DELETED + LABEL_UPDATED + PAGE_CREATED + PAGE_DELETED + PAGE_UPDATED +} + +union WebhookResult = WebhookError | WebhookSuccess + +type WebhookSuccess { + webhook: Webhook! +} + +type WebhooksError { + errorCodes: [WebhooksErrorCode!]! +} + +enum WebhooksErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union WebhooksResult = WebhooksError | WebhooksSuccess + +type WebhooksSuccess { + webhooks: [Webhook!]! +} diff --git a/android/SaveToOmnivore/app/src/main/ic_launcher-playstore.png b/android/SaveToOmnivore/app/src/main/ic_launcher-playstore.png new file mode 100644 index 000000000..3f5ca0237 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/ic_launcher-playstore.png differ diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/AppDatastore.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/AppDatastore.kt new file mode 100644 index 000000000..2ad6b5ded --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/AppDatastore.kt @@ -0,0 +1,45 @@ +package app.omnivore.savetoomnivore + +import android.annotation.SuppressLint +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class AppDatastore(private val context: Context) { + private val Context.datastore: DataStore by preferencesDataStore(name = "app_datastore") + private val APIKEY = stringPreferencesKey(name = "API_KEY") + + companion object { + @SuppressLint("StaticFieldLeak") + var INSTANCE: AppDatastore? = null + fun getInstance(base: Context): AppDatastore? { + if (INSTANCE == null) { + synchronized(AppDatastore::class.java) { + INSTANCE = AppDatastore(base.applicationContext) + } + } + + return INSTANCE + } + } + + suspend fun setApiKey(apiKey: String) { + context.datastore.edit { preferences -> + preferences[APIKEY] = apiKey + } + } + + val getApiKey: Flow = context.datastore.data.map { preferences -> + preferences[APIKEY] ?: "" + } +} \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ExtensionActivity.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ExtensionActivity.kt new file mode 100644 index 000000000..f0545f764 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ExtensionActivity.kt @@ -0,0 +1,106 @@ +package app.omnivore.savetoomnivore + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils.isEmpty +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import app.omnivore.generated.SaveUrlMutation +import app.omnivore.generated.type.SaveUrlInput +import com.apollographql.apollo3.ApolloClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern + + +private val Context.dataStore: DataStore by preferencesDataStore( + name = "settings" +) + +class ExtensionActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + when { + intent?.action == Intent.ACTION_SEND -> { + if ("text/plain" == intent.type) { + handleSendText(intent) // Handle text being sent + } + } + } + finish() + } + + fun saveURL(url: String) { + + GlobalScope.launch { + val apiKey = AppDatastore.getInstance(baseContext)?.getApiKey?.first() + + val apolloClient = ApolloClient.Builder() + .serverUrl("https://api-prod.omnivore.app/api/graphql") + .addHttpHeader( + "Authorization", + value = apiKey.toString() + ) + .build() + + val source = "android" + val clientRequestId = UUID.randomUUID().toString() + val response = apolloClient.mutation( + SaveUrlMutation( + SaveUrlInput( + clientRequestId = clientRequestId, + source = source, + url = url + ) + ) + ).execute() + + val success = (response.data?.saveUrl?.onSaveSuccess?.url != null) + + GlobalScope.launch(Dispatchers.Main) { + runOnUiThread(Runnable { + val message = if (success) "Saved to Omnivore" else "Error saving to Omnivore" + Toast.makeText(baseContext, message, Toast.LENGTH_LONG).show() + }) + } + } + } + + private fun handleSendText(intent: Intent) { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + val url = getUrl(it) + if (url == null || isEmpty(url)) { + Toast.makeText(this, "Error: no URL found.", Toast.LENGTH_LONG).show() + } else { + Toast.makeText(this, "Saving to Omnivore", Toast.LENGTH_LONG).show() + saveURL(it) + } + } + } + + fun getUrl(s: String): String? { + val pattern = + Pattern.compile("[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)") + try { + val matcher: Matcher = pattern.matcher(s) + if (matcher.find()) { + return s.substring(matcher.start(), matcher.end()) + } + } catch (e: Exception) { + System.out.println("exception parsing string") + System.out.println(e) + return null + } + return null + } +} diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/MainActivity.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/MainActivity.kt new file mode 100644 index 000000000..aaaff30e9 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/MainActivity.kt @@ -0,0 +1,63 @@ +package app.omnivore.savetoomnivore + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.material3.OutlinedTextField +import kotlinx.coroutines.launch +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.GlobalScope + +class MainActivity : ComponentActivity() { + + @RequiresApi(Build.VERSION_CODES.M) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MaterialTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text("Save to Omnivore") + var apiKey by remember { mutableStateOf("") } + OutlinedTextField( + value = apiKey, + onValueChange = { + apiKey = it + GlobalScope.launch { + setApiKey(apiKey) + } + }, + label = { Text("API Key") } + ) + Text("Get an API key from https://omnivore.app/settings/api") + } + } + } + } + } + + private suspend fun setApiKey(apiKey: String) { + AppDatastore.getInstance(base = baseContext)?.setApiKey(apiKey) + } +} diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Color.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Color.kt new file mode 100644 index 000000000..babd26191 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package app.omnivore.savetoomnivore.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Theme.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Theme.kt new file mode 100644 index 000000000..6dc8511dd --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Theme.kt @@ -0,0 +1,68 @@ +package app.omnivore.savetoomnivore.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.ViewCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun SaveToOmnivoreTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() + ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Type.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Type.kt new file mode 100644 index 000000000..be24a8304 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package app.omnivore.savetoomnivore.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/SaveToOmnivore/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/drawable/ic_launcher_background.xml b/android/SaveToOmnivore/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..6b3619d2e Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..84f3ca04b Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..a4756788b Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..f5837c7ba Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..43475955d Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..9cbca8c15 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..5caff3fed Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..f70fdcea9 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..95fddeab7 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..624d9e310 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..7873c2cb6 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..24ef75825 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..de87fd987 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..b386ab807 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..0227993e3 Binary files /dev/null and b/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/SaveToOmnivore/app/src/main/res/values/colors.xml b/android/SaveToOmnivore/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/values/ic_launcher_background.xml b/android/SaveToOmnivore/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..c5d5899fd --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/values/strings.xml b/android/SaveToOmnivore/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..e537d2ef1 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Omnivore + \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/values/themes.xml b/android/SaveToOmnivore/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..1a1372a95 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/res/values/themes.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/android/SaveToOmnivore/app/src/main/res/xml/backup_rules.xml b/android/SaveToOmnivore/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 000000000..fa0f996d2 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/xml/data_extraction_rules.xml b/android/SaveToOmnivore/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..9ee9997b0 --- /dev/null +++ b/android/SaveToOmnivore/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/test/java/app/omnivore/savetoomnivore/ExampleUnitTest.kt b/android/SaveToOmnivore/app/src/test/java/app/omnivore/savetoomnivore/ExampleUnitTest.kt new file mode 100644 index 000000000..332df63c4 --- /dev/null +++ b/android/SaveToOmnivore/app/src/test/java/app/omnivore/savetoomnivore/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package app.omnivore.savetoomnivore + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/android/SaveToOmnivore/build.gradle b/android/SaveToOmnivore/build.gradle new file mode 100644 index 000000000..caac89d71 --- /dev/null +++ b/android/SaveToOmnivore/build.gradle @@ -0,0 +1,14 @@ +buildscript { + ext { + compose_version = '1.1.0-beta01' + } +}// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.2.1' apply false + id 'com.android.library' version '7.2.1' apply false + id 'org.jetbrains.kotlin.android' version '1.6.20' apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/android/SaveToOmnivore/gradle.properties b/android/SaveToOmnivore/gradle.properties new file mode 100644 index 000000000..cd0519bb2 --- /dev/null +++ b/android/SaveToOmnivore/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.jar b/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e708b1c02 Binary files /dev/null and b/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.properties b/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..9e79af52e --- /dev/null +++ b/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jul 19 16:31:46 PDT 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/android/SaveToOmnivore/gradlew b/android/SaveToOmnivore/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/android/SaveToOmnivore/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/android/SaveToOmnivore/gradlew.bat b/android/SaveToOmnivore/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/android/SaveToOmnivore/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/SaveToOmnivore/settings.gradle b/android/SaveToOmnivore/settings.gradle new file mode 100644 index 000000000..40d2999e2 --- /dev/null +++ b/android/SaveToOmnivore/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "SaveToOmnivore" +include ':app'