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'