Merge all changes from main, update theming of Discover
@ -33,6 +33,7 @@
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||
"@typescript-eslint/no-unsafe-return": "warn"
|
||||
"@typescript-eslint/no-unsafe-return": "warn",
|
||||
"@typescript-eslint/no-unsafe-argument": "warn"
|
||||
}
|
||||
}
|
||||
|
||||
32
.github/workflows/build-docker-images.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: Run tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'apple/**'
|
||||
- 'android/**'
|
||||
|
||||
jobs:
|
||||
build-docker-images:
|
||||
name: Build docker images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Login to GitHub container registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{github.actor}}
|
||||
password: ${{secrets.GITHUB_TOKEN}}
|
||||
- name: Build the backend docker image
|
||||
run: |
|
||||
docker build . --file packages/api/Dockerfile --tag "ghcr.io/omnivore-app/backend:${GITHUB_SHA}" --tag ghcr.io/omnivore-app/backend:latest
|
||||
docker push ghcr.io/omnivore-app/backend:${GITHUB_SHA}
|
||||
- name: Build the content-fetch docker image
|
||||
run: |
|
||||
docker build --file packages/content-fetch/Dockerfile . --tag "ghcr.io/omnivore-app/content-fetch:${GITHUB_SHA}" --tag ghcr.io/omnivore-app/content-fetch:latest
|
||||
docker push ghcr.io/omnivore-app/content-fetch:${GITHUB_SHA}
|
||||
6
.github/workflows/run-tests.yaml
vendored
@ -5,11 +5,13 @@ on:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'apple/**'
|
||||
- 'android/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'apple/**'
|
||||
- 'android/**'
|
||||
|
||||
env:
|
||||
NEXT_PUBLIC_APP_ENV: prod
|
||||
@ -93,11 +95,13 @@ jobs:
|
||||
PG_DB: omnivore_test
|
||||
PG_LOGGER: debug
|
||||
REDIS_URL: redis://localhost:${{ job.services.redis.ports[6379] }}
|
||||
MQ_REDIS_URL: redis://localhost:${{ job.services.redis.ports[6379] }}
|
||||
build-docker-images:
|
||||
name: Build docker images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build the API docker image
|
||||
|
||||
@ -50,13 +50,13 @@ Omnivore takes advantage of some great open source software:
|
||||
- [Stitches](https://stitches.dev/) - We use Stitches on the frontend to style our components.
|
||||
- [Mozilla Readability](https://github.com/mozilla/readability) - We use Mozilla's Readability library to make pages easier to read.
|
||||
- [Swift GraphQL](https://www.swift-graphql.com/) - We generate our GraphQL queries on iOS using Swift GraphQL.
|
||||
- [Apollo GraphQL](https://www.apollographql.com/) - We generate our GraphQL queries on Android using Apollo GraphQL.
|
||||
- [Radix](https://www.radix-ui.com/) - We use Radix UI's components on our frontend.
|
||||
- And many more awesome libraries, just checkout our package files to see what we are using.
|
||||
|
||||
## Importing Libraries
|
||||
|
||||
If you have a library you'd like to import, [@davidohlin](https://github.com/davidohlin) has created
|
||||
a tool that imports a list of CSV URLs: [omnivore-import](https://github.com/davidohlin/instapaper-to-omnivore-import)
|
||||
Check out our [docs](https://docs.omnivore.app/using/importing.html) for information on importing your data from other apps.
|
||||
|
||||
## How to setup local development :computer:
|
||||
|
||||
@ -66,7 +66,7 @@ The easiest way to get started with local development is to use `docker compose
|
||||
|
||||
Omnivore is written in TypeScript and JavaScript.
|
||||
|
||||
- [Node](https://nodejs.org/) -- currently we are using Node.js v14.18
|
||||
- [Node](https://nodejs.org/) -- currently we are using Node.js v18.16
|
||||
- [Chromium](https://www.chromium.org/chromium-projects/) -- see below for installation info
|
||||
|
||||
### Running the web and API services
|
||||
|
||||
@ -1,172 +0,0 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'kotlin-kapt'
|
||||
id 'dagger.hilt.android.plugin'
|
||||
id 'com.apollographql.apollo3' version '3.7.2'
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("app/external/keystore.properties");
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId "app.omnivore.omnivore"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 158
|
||||
versionName "0.0.158"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias 'key0'
|
||||
storeFile file('external/omnivore-prod.keystore')
|
||||
storePassword keystoreProperties['prodStorePassword']
|
||||
keyPassword keystoreProperties['prodKeyPassword']
|
||||
}
|
||||
debug {
|
||||
if (keystoreProperties['demoStorePassword'] && keystoreProperties['demoKeyPassword']) {
|
||||
keyAlias 'androiddebugkey'
|
||||
storeFile file('external/omnivore-demo.keystore')
|
||||
storePassword keystoreProperties['demoStorePassword']
|
||||
keyPassword keystoreProperties['demoKeyPassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
buildConfigField("String", "OMNIVORE_API_URL", "\"https://api-demo.omnivore.app\"")
|
||||
buildConfigField("String", "OMNIVORE_WEB_URL", "\"https://demo.omnivore.app\"")
|
||||
buildConfigField("String", "OMNIVORE_GAUTH_SERVER_CLIENT_ID", "\"267918240109-eu2ar09unac3lqqigluknhk7t0021b54.apps.googleusercontent.com\"")
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
signingConfig signingConfigs.release
|
||||
buildConfigField("String", "OMNIVORE_API_URL", "\"https://api-prod.omnivore.app\"")
|
||||
buildConfigField("String", "OMNIVORE_WEB_URL", "\"https://omnivore.app\"")
|
||||
buildConfigField("String", "OMNIVORE_GAUTH_SERVER_CLIENT_ID", "\"687911924401-lq8j1e97n0sv3khhb8g8n368lk4dqkbp.apps.googleusercontent.com\"")
|
||||
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.3.1'
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
namespace 'app.omnivore.omnivore'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def nav_version = "2.5.3"
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation "androidx.compose.ui:ui:$compose_version"
|
||||
implementation "androidx.compose.material:material:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose_version"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
|
||||
implementation 'androidx.activity:activity-compose:1.6.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'com.google.android.gms:play-services-base:18.1.0'
|
||||
|
||||
implementation "androidx.navigation:navigation-compose:$nav_version"
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.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"
|
||||
|
||||
// Jetpack Lifecycle deps
|
||||
|
||||
// ViewModel
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
|
||||
// ViewModel utilities for Compose
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
|
||||
// LiveData
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
|
||||
implementation("androidx.compose.runtime:runtime-livedata:1.3.2")
|
||||
|
||||
// Saved state module for ViewModel
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version")
|
||||
|
||||
// Annotation processor
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version")
|
||||
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
|
||||
// coroutines
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
|
||||
|
||||
implementation "androidx.security:security-crypto:1.0.0"
|
||||
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||
|
||||
//Dagger - Hilt
|
||||
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||
kapt "com.google.dagger:hilt-compiler:$hilt_version"
|
||||
|
||||
implementation 'com.apollographql.apollo3:apollo-runtime:3.7.2'
|
||||
|
||||
implementation 'androidx.compose.material3:material3:1.1.2'
|
||||
implementation 'androidx.compose.material3:material3-window-size-class:1.1.2'
|
||||
|
||||
implementation 'com.google.android.gms:play-services-auth:20.4.0'
|
||||
implementation "com.google.accompanist:accompanist-systemuicontroller:0.25.1"
|
||||
implementation "com.google.accompanist:accompanist-flowlayout:0.25.1"
|
||||
|
||||
implementation 'io.coil-kt:coil-compose:2.3.0'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'com.pspdfkit:pspdfkit:8.9.1'
|
||||
|
||||
implementation 'com.posthog.android:posthog:2.0.3'
|
||||
implementation 'io.intercom.android:intercom-sdk:15.1.0'
|
||||
|
||||
// Room Deps
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
|
||||
implementation 'com.github.jeziellago:compose-markdown:0.3.3'
|
||||
implementation "io.github.dokar3:chiptextfield:0.4.7"
|
||||
}
|
||||
|
||||
apollo {
|
||||
packageName.set 'app.omnivore.omnivore.graphql.generated'
|
||||
}
|
||||
|
||||
task printVersion {
|
||||
doLast {
|
||||
println "omnivoreVersion: ${android.defaultConfig.versionName}"
|
||||
}
|
||||
}
|
||||
186
android/Omnivore/app/build.gradle.kts
Normal file
@ -0,0 +1,186 @@
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
kotlin("android")
|
||||
id("dagger.hilt.android.plugin")
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.apollo)
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("app/external/keystore.properties")
|
||||
val keystoreProperties = Properties()
|
||||
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
FileInputStream(keystorePropertiesFile).use { input ->
|
||||
keystoreProperties.load(input)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.omnivore.omnivore"
|
||||
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.omnivore.omnivore"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 194001
|
||||
versionName = "0.195.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = "key0"
|
||||
storeFile = file("external/omnivore-prod.keystore")
|
||||
storePassword = keystoreProperties["prodStorePassword"] as String?
|
||||
keyPassword = keystoreProperties["prodKeyPassword"] as String?
|
||||
}/* debug {
|
||||
if (keystoreProperties["demoStorePassword"] && keystoreProperties["demoKeyPassword"]) {
|
||||
keyAlias = "androiddebugkey"
|
||||
storeFile = file("external/omnivore-demo.keystore")
|
||||
storePassword = keystoreProperties["demoStorePassword"] as String
|
||||
keyPassword = keystoreProperties["demoKeyPassword"] as String
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
applicationIdSuffix = ".debug"
|
||||
buildConfigField("String", "OMNIVORE_API_URL", "\"https://api-demo.omnivore.app\"")
|
||||
buildConfigField("String", "OMNIVORE_WEB_URL", "\"https://demo.omnivore.app\"")
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OMNIVORE_GAUTH_SERVER_CLIENT_ID",
|
||||
"\"267918240109-eu2ar09unac3lqqigluknhk7t0021b54.apps.googleusercontent.com\""
|
||||
)
|
||||
}
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
buildConfigField("String", "OMNIVORE_API_URL", "\"https://api-prod.omnivore.app\"")
|
||||
buildConfigField("String", "OMNIVORE_WEB_URL", "\"https://omnivore.app\"")
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OMNIVORE_GAUTH_SERVER_CLIENT_ID",
|
||||
"\"687911924401-lq8j1e97n0sv3khhb8g8n368lk4dqkbp.apps.googleusercontent.com\""
|
||||
)
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get()
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += listOf("/META-INF/{AL2.0,LGPL2.1}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
implementation(libs.gms.playServicesBase)
|
||||
implementation(libs.gms.playServicesAuth)
|
||||
|
||||
val bom = platform(libs.androidx.compose.bom)
|
||||
implementation(bom)
|
||||
androidTestImplementation(bom)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.ui.util)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
androidTestImplementation(libs.androidx.compose.ui.test)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling.preview)
|
||||
debugImplementation(libs.androidx.compose.ui.testManifest)
|
||||
|
||||
testImplementation(libs.junit4)
|
||||
androidTestImplementation(libs.androidx.test.ext)
|
||||
androidTestImplementation(libs.androidx.test.espresso.core)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewModelKtx)
|
||||
implementation(libs.androidx.lifecycle.viewModelCompose)
|
||||
implementation(libs.androidx.lifecycle.viewmodelSavedstate)
|
||||
|
||||
implementation(libs.androidx.lifecycle.livedata.ktx)
|
||||
implementation(libs.androidx.compose.runtime.livedata)
|
||||
|
||||
implementation(libs.retrofit.core)
|
||||
implementation(libs.retrofit.converter.gson)
|
||||
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(libs.androidx.dataStore.preferences)
|
||||
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
implementation(libs.apollo.runtime)
|
||||
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
|
||||
implementation(libs.coil.kt.compose)
|
||||
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
annotationProcessor(libs.room.compiler)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
implementation(libs.gson)
|
||||
implementation(libs.pspdfkit)
|
||||
implementation(libs.posthog)
|
||||
implementation(libs.intercom)
|
||||
implementation(libs.compose.markdown)
|
||||
implementation(libs.chiptextfield.m3)
|
||||
|
||||
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
|
||||
}
|
||||
|
||||
apollo {
|
||||
service("service") {
|
||||
outputDirConnection {
|
||||
connectToKotlinSourceSet("main")
|
||||
}
|
||||
packageName.set("app.omnivore.omnivore.graphql.generated")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("printVersion") {
|
||||
doLast {
|
||||
println("omnivoreVersion: ${android.defaultConfig.versionName}")
|
||||
}
|
||||
}
|
||||
4
android/Omnivore/app/proguard-rules.pro
vendored
@ -1,6 +1,6 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
@ -18,4 +18,4 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
BIN
android/Omnivore/app/src/debug/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
android/Omnivore/app/src/debug/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
BIN
android/Omnivore/app/src/debug/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 966 B |
|
After Width: | Height: | Size: 1.7 KiB |
BIN
android/Omnivore/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
4
android/Omnivore/app/src/debug/res/values/strings.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dev - Omnivore</string>
|
||||
</resources>
|
||||
@ -15,14 +15,15 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Omnivore"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/Theme.AppCompat.Translucent"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Omnivore">
|
||||
android:theme="@style/Theme.Omnivore.Splash"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@ -30,9 +31,8 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.save.SaveSheetActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppCompat.Translucent">
|
||||
android:name=".feature.save.SaveSheetActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@ -46,12 +46,12 @@
|
||||
android:windowSoftInputMode="adjustNothing" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.reader.PDFReaderActivity"
|
||||
android:name=".feature.reader.PDFReaderActivity"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar"
|
||||
android:windowSoftInputMode="adjustNothing" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.reader.WebReaderLoadingContainerActivity"
|
||||
android:name=".feature.reader.WebReaderLoadingContainerActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Omnivore"/>
|
||||
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
query UpdatesSince($after: String, $first: Int, $since: Date!) {
|
||||
updatesSince(after: $after, first: $first, since: $since) {
|
||||
query UpdatesSince(
|
||||
$folder: String
|
||||
$after: String
|
||||
$first: Int
|
||||
$since: Date!
|
||||
) {
|
||||
updatesSince(after: $after, first: $first, folder: $folder, since: $since) {
|
||||
... on UpdatesSinceSuccess {
|
||||
edges {
|
||||
cursor
|
||||
|
||||
@ -69,6 +69,7 @@ type Article {
|
||||
contentReader: ContentReader!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
folder: String!
|
||||
hasContent: Boolean
|
||||
hash: String!
|
||||
highlights(input: ArticleHighlightsInput): [Highlight!]!
|
||||
@ -154,6 +155,7 @@ union ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequ
|
||||
|
||||
enum ArticleSavingRequestStatus {
|
||||
ARCHIVED
|
||||
CONTENT_NOT_FETCHED
|
||||
DELETED
|
||||
FAILED
|
||||
PROCESSING
|
||||
@ -203,6 +205,7 @@ enum BulkActionType {
|
||||
ARCHIVE
|
||||
DELETE
|
||||
MARK_AS_READ
|
||||
MOVE_TO_FOLDER
|
||||
}
|
||||
|
||||
enum ContentReader {
|
||||
@ -227,8 +230,12 @@ enum CreateArticleErrorCode {
|
||||
|
||||
input CreateArticleInput {
|
||||
articleSavingRequestId: ID
|
||||
folder: String
|
||||
labels: [CreateLabelInput!]
|
||||
preparedDocument: PreparedDocumentInput
|
||||
publishedAt: Date
|
||||
rssFeedUrl: String
|
||||
savedAt: Date
|
||||
skipParsing: Boolean
|
||||
source: String
|
||||
state: ArticleSavingRequestStatus
|
||||
@ -377,6 +384,12 @@ enum CreateNewsletterEmailErrorCode {
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input CreateNewsletterEmailInput {
|
||||
description: String
|
||||
folder: String
|
||||
name: String
|
||||
}
|
||||
|
||||
union CreateNewsletterEmailResult = CreateNewsletterEmailError | CreateNewsletterEmailSuccess
|
||||
|
||||
type CreateNewsletterEmailSuccess {
|
||||
@ -631,6 +644,20 @@ type DeviceTokensSuccess {
|
||||
deviceTokens: [DeviceToken!]!
|
||||
}
|
||||
|
||||
type EmptyTrashError {
|
||||
errorCodes: [EmptyTrashErrorCode!]!
|
||||
}
|
||||
|
||||
enum EmptyTrashErrorCode {
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union EmptyTrashResult = EmptyTrashError | EmptyTrashSuccess
|
||||
|
||||
type EmptyTrashSuccess {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
type Feature {
|
||||
createdAt: Date!
|
||||
expiresAt: Date
|
||||
@ -641,6 +668,19 @@ type Feature {
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
type Feed {
|
||||
author: String
|
||||
createdAt: Date
|
||||
description: String
|
||||
id: ID
|
||||
image: String
|
||||
publishedAt: Date
|
||||
title: String!
|
||||
type: String
|
||||
updatedAt: Date
|
||||
url: String!
|
||||
}
|
||||
|
||||
type FeedArticle {
|
||||
annotationsCount: Int
|
||||
article: Article!
|
||||
@ -674,12 +714,56 @@ type FeedArticlesSuccess {
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type FeedEdge {
|
||||
cursor: String!
|
||||
node: Feed!
|
||||
}
|
||||
|
||||
type FeedsError {
|
||||
errorCodes: [FeedsErrorCode!]!
|
||||
}
|
||||
|
||||
enum FeedsErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input FeedsInput {
|
||||
after: String
|
||||
first: Int
|
||||
query: String
|
||||
sort: SortParams
|
||||
}
|
||||
|
||||
union FeedsResult = FeedsError | FeedsSuccess
|
||||
|
||||
type FeedsSuccess {
|
||||
edges: [FeedEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type FetchContentError {
|
||||
errorCodes: [FetchContentErrorCode!]!
|
||||
}
|
||||
|
||||
enum FetchContentErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union FetchContentResult = FetchContentError | FetchContentSuccess
|
||||
|
||||
type FetchContentSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Filter {
|
||||
category: String!
|
||||
category: String
|
||||
createdAt: Date!
|
||||
defaultFilter: Boolean
|
||||
description: String
|
||||
filter: String!
|
||||
folder: String
|
||||
id: ID!
|
||||
name: String!
|
||||
position: Int!
|
||||
@ -901,6 +985,8 @@ type IntegrationsSuccess {
|
||||
integrations: [Integration!]!
|
||||
}
|
||||
|
||||
scalar JSON
|
||||
|
||||
type JoinGroupError {
|
||||
errorCodes: [JoinGroupErrorCode!]!
|
||||
}
|
||||
@ -925,6 +1011,7 @@ type Label {
|
||||
internal: Boolean
|
||||
name: String!
|
||||
position: Int
|
||||
source: String
|
||||
}
|
||||
|
||||
type LabelsError {
|
||||
@ -1107,15 +1194,31 @@ type MoveLabelSuccess {
|
||||
label: Label!
|
||||
}
|
||||
|
||||
type MoveToFolderError {
|
||||
errorCodes: [MoveToFolderErrorCode!]!
|
||||
}
|
||||
|
||||
enum MoveToFolderErrorCode {
|
||||
ALREADY_EXISTS
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union MoveToFolderResult = MoveToFolderError | MoveToFolderSuccess
|
||||
|
||||
type MoveToFolderSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
addPopularRead(name: String!): AddPopularReadResult!
|
||||
bulkAction(action: BulkActionType!, async: Boolean, expectedCount: Int, labelIds: [ID!], query: String!): BulkActionResult!
|
||||
bulkAction(action: BulkActionType!, arguments: JSON, async: Boolean, expectedCount: Int, labelIds: [ID!], query: String!): BulkActionResult!
|
||||
createArticle(input: CreateArticleInput!): CreateArticleResult!
|
||||
createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult!
|
||||
createGroup(input: CreateGroupInput!): CreateGroupResult!
|
||||
createHighlight(input: CreateHighlightInput!): CreateHighlightResult!
|
||||
createLabel(input: CreateLabelInput!): CreateLabelResult!
|
||||
createNewsletterEmail: CreateNewsletterEmailResult!
|
||||
createNewsletterEmail(input: CreateNewsletterEmailInput): CreateNewsletterEmailResult!
|
||||
deleteAccount(userID: ID!): DeleteAccountResult!
|
||||
deleteFilter(id: ID!): DeleteFilterResult!
|
||||
deleteHighlight(highlightId: ID!): DeleteHighlightResult!
|
||||
@ -1124,6 +1227,8 @@ type Mutation {
|
||||
deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
|
||||
deleteRule(id: ID!): DeleteRuleResult!
|
||||
deleteWebhook(id: ID!): DeleteWebhookResult!
|
||||
emptyTrash: EmptyTrashResult!
|
||||
fetchContent(id: ID!): FetchContentResult!
|
||||
generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult!
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
googleSignup(input: GoogleSignupInput!): GoogleSignupResult!
|
||||
@ -1135,6 +1240,7 @@ type Mutation {
|
||||
mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult!
|
||||
moveFilter(input: MoveFilterInput!): MoveFilterResult!
|
||||
moveLabel(input: MoveLabelInput!): MoveLabelResult!
|
||||
moveToFolder(folder: String!, id: ID!): MoveToFolderResult!
|
||||
optInFeature(input: OptInFeatureInput!): OptInFeatureResult!
|
||||
recommend(input: RecommendInput!): RecommendResult!
|
||||
recommendHighlights(input: RecommendHighlightsInput!): RecommendHighlightsResult!
|
||||
@ -1161,6 +1267,7 @@ type Mutation {
|
||||
updateFilter(input: UpdateFilterInput!): UpdateFilterResult!
|
||||
updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult!
|
||||
updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
|
||||
updateNewsletterEmail(input: UpdateNewsletterEmailInput!): UpdateNewsletterEmailResult!
|
||||
updatePage(input: UpdatePageInput!): UpdatePageResult!
|
||||
updateSubscription(input: UpdateSubscriptionInput!): UpdateSubscriptionResult!
|
||||
updateUser(input: UpdateUserInput!): UpdateUserResult!
|
||||
@ -1173,7 +1280,10 @@ type NewsletterEmail {
|
||||
address: String!
|
||||
confirmationCode: String
|
||||
createdAt: Date!
|
||||
description: String
|
||||
folder: String!
|
||||
id: ID!
|
||||
name: String
|
||||
subscriptionCount: Int!
|
||||
}
|
||||
|
||||
@ -1292,6 +1402,7 @@ type Query {
|
||||
article(format: String, slug: String!, username: String!): ArticleResult!
|
||||
articleSavingRequest(id: ID, url: String): ArticleSavingRequestResult!
|
||||
deviceTokens: DeviceTokensResult!
|
||||
feeds(input: FeedsInput!): FeedsResult!
|
||||
filters: FiltersResult!
|
||||
getUserPersonalization: GetUserPersonalizationResult!
|
||||
groups: GroupsResult!
|
||||
@ -1303,11 +1414,12 @@ type Query {
|
||||
recentEmails: RecentEmailsResult!
|
||||
recentSearches: RecentSearchesResult!
|
||||
rules(enabled: Boolean): RulesResult!
|
||||
scanFeeds(input: ScanFeedsInput!): ScanFeedsResult!
|
||||
search(after: String, first: Int, format: String, includeContent: Boolean, query: String): SearchResult!
|
||||
sendInstallInstructions: SendInstallInstructionsResult!
|
||||
subscriptions(sort: SortParams, type: SubscriptionType): SubscriptionsResult!
|
||||
typeaheadSearch(first: Int, query: String!): TypeaheadSearchResult!
|
||||
updatesSince(after: String, first: Int, since: Date!, sort: SortParams): UpdatesSinceResult!
|
||||
updatesSince(after: String, first: Int, folder: String, since: Date!, sort: SortParams): UpdatesSinceResult!
|
||||
user(userId: ID, username: String): UserResult!
|
||||
users: UsersResult!
|
||||
validateUsername(username: String!): Boolean!
|
||||
@ -1604,6 +1716,7 @@ enum SaveErrorCode {
|
||||
|
||||
input SaveFileInput {
|
||||
clientRequestId: ID!
|
||||
folder: String
|
||||
labels: [CreateLabelInput!]
|
||||
source: String!
|
||||
state: ArticleSavingRequestStatus
|
||||
@ -1625,6 +1738,7 @@ input SaveFilterInput {
|
||||
category: String
|
||||
description: String
|
||||
filter: String!
|
||||
folder: String
|
||||
name: String!
|
||||
position: Int
|
||||
}
|
||||
@ -1637,6 +1751,7 @@ type SaveFilterSuccess {
|
||||
|
||||
input SavePageInput {
|
||||
clientRequestId: ID!
|
||||
folder: String
|
||||
labels: [CreateLabelInput!]
|
||||
originalContent: String!
|
||||
parseResult: ParseResult
|
||||
@ -1658,6 +1773,7 @@ type SaveSuccess {
|
||||
|
||||
input SaveUrlInput {
|
||||
clientRequestId: ID!
|
||||
folder: String
|
||||
labels: [CreateLabelInput!]
|
||||
locale: String
|
||||
publishedAt: Date
|
||||
@ -1668,6 +1784,25 @@ input SaveUrlInput {
|
||||
url: String!
|
||||
}
|
||||
|
||||
type ScanFeedsError {
|
||||
errorCodes: [ScanFeedsErrorCode!]!
|
||||
}
|
||||
|
||||
enum ScanFeedsErrorCode {
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
input ScanFeedsInput {
|
||||
opml: String
|
||||
url: String
|
||||
}
|
||||
|
||||
union ScanFeedsResult = ScanFeedsError | ScanFeedsSuccess
|
||||
|
||||
type ScanFeedsSuccess {
|
||||
feeds: [Feed!]!
|
||||
}
|
||||
|
||||
type SearchError {
|
||||
errorCodes: [SearchErrorCode!]!
|
||||
}
|
||||
@ -1686,16 +1821,20 @@ type SearchItem {
|
||||
contentReader: ContentReader!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
folder: String!
|
||||
highlights: [Highlight!]
|
||||
id: ID!
|
||||
image: String
|
||||
isArchived: Boolean!
|
||||
labels: [Label!]
|
||||
language: String
|
||||
links: JSON
|
||||
originalArticleUrl: String
|
||||
ownedByViewer: Boolean
|
||||
pageId: ID
|
||||
pageType: PageType!
|
||||
previewContent: String
|
||||
previewContentType: String
|
||||
publishedAt: Date
|
||||
quote: String
|
||||
readAt: Date
|
||||
@ -1844,6 +1983,7 @@ input SetIntegrationInput {
|
||||
importItemState: ImportItemState
|
||||
name: String!
|
||||
syncedAt: Date
|
||||
taskName: String
|
||||
token: String!
|
||||
type: IntegrationType
|
||||
}
|
||||
@ -1874,6 +2014,7 @@ input SetLabelsInput {
|
||||
labelIds: [ID!]
|
||||
labels: [CreateLabelInput!]
|
||||
pageId: ID!
|
||||
source: String
|
||||
}
|
||||
|
||||
union SetLabelsResult = SetLabelsError | SetLabelsSuccess
|
||||
@ -1963,6 +2104,7 @@ enum SetUserPersonalizationErrorCode {
|
||||
}
|
||||
|
||||
input SetUserPersonalizationInput {
|
||||
fields: JSON
|
||||
fontFamily: String
|
||||
fontSize: Int
|
||||
libraryLayoutType: String
|
||||
@ -2068,6 +2210,10 @@ enum SubscribeErrorCode {
|
||||
}
|
||||
|
||||
input SubscribeInput {
|
||||
autoAddToLibrary: Boolean
|
||||
fetchContent: Boolean
|
||||
folder: String
|
||||
isPrivate: Boolean
|
||||
subscriptionType: SubscriptionType
|
||||
url: String!
|
||||
}
|
||||
@ -2079,11 +2225,15 @@ type SubscribeSuccess {
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
autoAddToLibrary: Boolean
|
||||
count: Int!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
fetchContent: Boolean!
|
||||
folder: String!
|
||||
icon: String
|
||||
id: ID!
|
||||
isPrivate: Boolean
|
||||
lastFetchedAt: Date
|
||||
name: String!
|
||||
newsletterEmail: String
|
||||
@ -2203,6 +2353,7 @@ input UpdateFilterInput {
|
||||
category: String
|
||||
description: String
|
||||
filter: String
|
||||
folder: String
|
||||
id: String!
|
||||
name: String
|
||||
position: Int
|
||||
@ -2307,6 +2458,28 @@ type UpdateLinkShareInfoSuccess {
|
||||
message: String!
|
||||
}
|
||||
|
||||
type UpdateNewsletterEmailError {
|
||||
errorCodes: [UpdateNewsletterEmailErrorCode!]!
|
||||
}
|
||||
|
||||
enum UpdateNewsletterEmailErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input UpdateNewsletterEmailInput {
|
||||
description: String
|
||||
folder: String
|
||||
id: ID!
|
||||
name: String
|
||||
}
|
||||
|
||||
union UpdateNewsletterEmailResult = UpdateNewsletterEmailError | UpdateNewsletterEmailSuccess
|
||||
|
||||
type UpdateNewsletterEmailSuccess {
|
||||
newsletterEmail: NewsletterEmail!
|
||||
}
|
||||
|
||||
type UpdatePageError {
|
||||
errorCodes: [UpdatePageErrorCode!]!
|
||||
}
|
||||
@ -2397,8 +2570,12 @@ enum UpdateSubscriptionErrorCode {
|
||||
}
|
||||
|
||||
input UpdateSubscriptionInput {
|
||||
autoAddToLibrary: Boolean
|
||||
description: String
|
||||
fetchContent: Boolean
|
||||
folder: String
|
||||
id: ID!
|
||||
isPrivate: Boolean
|
||||
lastFetchedAt: Date
|
||||
lastFetchedChecksum: String
|
||||
name: String
|
||||
@ -2557,6 +2734,7 @@ enum UserErrorCode {
|
||||
}
|
||||
|
||||
type UserPersonalization {
|
||||
fields: JSON
|
||||
fontFamily: String
|
||||
fontSize: Int
|
||||
id: ID
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
package app.omnivore.omnivore
|
||||
|
||||
import android.content.Context
|
||||
import com.posthog.android.PostHog
|
||||
import com.posthog.android.Properties
|
||||
import io.intercom.android.sdk.Intercom
|
||||
import io.intercom.android.sdk.identity.Registration
|
||||
import org.json.JSONObject
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class EventTracker @Inject constructor(val app: Context) {
|
||||
private val posthog: PostHog
|
||||
|
||||
init {
|
||||
val posthogClientKey = app.getString(R.string.posthog_client_key)
|
||||
val posthogInstanceAddress = app.getString(R.string.posthog_instance_address)
|
||||
|
||||
posthog = PostHog.Builder(app, posthogClientKey, posthogInstanceAddress)
|
||||
.captureApplicationLifecycleEvents()
|
||||
.collectDeviceId(false)
|
||||
.build()
|
||||
|
||||
PostHog.setSingletonInstance(posthog)
|
||||
}
|
||||
|
||||
fun registerUser(userID: String, intercomHash: String?, isDebug: Boolean) {
|
||||
posthog.identify(userID)
|
||||
if (!isDebug) {
|
||||
Intercom.client().loginIdentifiedUser(Registration.create().withUserId(userID))
|
||||
intercomHash?.let { intercomHash ->
|
||||
Intercom.client().setUserHash(intercomHash)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun track(eventName: String, properties: Properties = Properties()) {
|
||||
posthog.capture(eventName, properties)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
posthog.reset()
|
||||
}
|
||||
}
|
||||
@ -4,81 +4,83 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.ui.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.ui.editinfo.EditInfoViewModel
|
||||
import app.omnivore.omnivore.ui.library.LibraryViewModel
|
||||
import app.omnivore.omnivore.ui.library.SearchViewModel
|
||||
import app.omnivore.omnivore.ui.root.RootView
|
||||
import app.omnivore.omnivore.ui.save.SaveViewModel
|
||||
import app.omnivore.omnivore.ui.settings.SettingsViewModel
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
import app.omnivore.omnivore.feature.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.feature.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
|
||||
import app.omnivore.omnivore.feature.library.SearchViewModel
|
||||
import app.omnivore.omnivore.feature.root.RootView
|
||||
import app.omnivore.omnivore.feature.save.SaveViewModel
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import com.pspdfkit.PSPDFKit
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
val loginViewModel: LoginViewModel by viewModels()
|
||||
val libraryViewModel: LibraryViewModel by viewModels()
|
||||
val settingsViewModel: SettingsViewModel by viewModels()
|
||||
val searchViewModel: SearchViewModel by viewModels()
|
||||
val labelsViewModel: LabelsViewModel by viewModels()
|
||||
val saveViewModel: SaveViewModel by viewModels()
|
||||
val editInfoViewModel: EditInfoViewModel by viewModels()
|
||||
installSplashScreen()
|
||||
|
||||
val context = this
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val licenseKey = getString(R.string.pspdfkit_license_key)
|
||||
val loginViewModel: LoginViewModel by viewModels()
|
||||
val searchViewModel: SearchViewModel by viewModels()
|
||||
val labelsViewModel: LabelsViewModel by viewModels()
|
||||
val saveViewModel: SaveViewModel by viewModels()
|
||||
val editInfoViewModel: EditInfoViewModel by viewModels()
|
||||
|
||||
if (licenseKey.length > 30) {
|
||||
PSPDFKit.initialize(context, licenseKey)
|
||||
} else {
|
||||
PSPDFKit.initialize(context, null)
|
||||
}
|
||||
}
|
||||
val context = this
|
||||
|
||||
setContent {
|
||||
OmnivoreTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = Color.Black)
|
||||
) {
|
||||
RootView(
|
||||
loginViewModel,
|
||||
searchViewModel,
|
||||
libraryViewModel,
|
||||
settingsViewModel,
|
||||
labelsViewModel,
|
||||
saveViewModel,
|
||||
editInfoViewModel)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val licenseKey = getString(R.string.pspdfkit_license_key)
|
||||
|
||||
if (licenseKey.length > 30) {
|
||||
PSPDFKit.initialize(context, licenseKey)
|
||||
} else {
|
||||
PSPDFKit.initialize(context, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// animate the view up when keyboard appears
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val rootView = findViewById<View>(android.R.id.content).rootView
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
|
||||
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
rootView.setPadding(0, 0, 0, imeHeight)
|
||||
insets
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
|
||||
OmnivoreTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
RootView(
|
||||
loginViewModel,
|
||||
searchViewModel,
|
||||
labelsViewModel,
|
||||
saveViewModel,
|
||||
editInfoViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// animate the view up when keyboard appears
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val rootView = findViewById<View>(android.R.id.content).rootView
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
|
||||
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
rootView.setPadding(0, 0, 0, imeHeight)
|
||||
insets
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
package app.omnivore.omnivore
|
||||
|
||||
sealed class Routes(val route: String) {
|
||||
object Library : Routes("Library")
|
||||
object Settings: Routes("Settings")
|
||||
object Search: Routes("Search")
|
||||
object Documentation: Routes("Documentation")
|
||||
object PrivacyPolicy: Routes("PrivacyPolicy")
|
||||
object TermsAndConditions: Routes("TermsAndConditions")
|
||||
object Notebook: Routes("Notebook")
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package app.omnivore.omnivore.core.analytics
|
||||
|
||||
import android.content.Context
|
||||
import app.omnivore.omnivore.R
|
||||
import com.posthog.android.PostHog
|
||||
import com.posthog.android.Properties
|
||||
import io.intercom.android.sdk.Intercom
|
||||
import io.intercom.android.sdk.identity.Registration
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class EventTracker @Inject constructor(val app: Context) {
|
||||
private val posthog: PostHog
|
||||
|
||||
init {
|
||||
val posthogClientKey = app.getString(R.string.posthog_client_key)
|
||||
val posthogInstanceAddress = app.getString(R.string.posthog_instance_address)
|
||||
|
||||
posthog = PostHog.Builder(app, posthogClientKey, posthogInstanceAddress)
|
||||
.captureApplicationLifecycleEvents()
|
||||
.collectDeviceId(false)
|
||||
.build()
|
||||
|
||||
PostHog.setSingletonInstance(posthog)
|
||||
}
|
||||
|
||||
fun registerUser(userID: String, intercomHash: String?, isDebug: Boolean) {
|
||||
posthog.identify(userID)
|
||||
if (!isDebug) {
|
||||
Intercom.client().loginIdentifiedUser(Registration.create().withUserId(userID))
|
||||
intercomHash?.let { intercomHash ->
|
||||
Intercom.client().setUserHash(intercomHash)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun track(eventName: String, properties: Properties = Properties()) {
|
||||
posthog.capture(eventName, properties)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
posthog.reset()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import app.omnivore.omnivore.core.database.OmnivoreDatabase
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class DataService @Inject constructor(
|
||||
val networker: Networker,
|
||||
omnivoreDatabase: OmnivoreDatabase
|
||||
) {
|
||||
val savedItemSyncChannel = Channel<SavedItem>(capacity = Channel.UNLIMITED)
|
||||
|
||||
val db = omnivoreDatabase
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
startSyncChannels()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearDatabase() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
db.clearAllTables()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,185 @@
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef
|
||||
import app.omnivore.omnivore.core.database.entities.saveHighlightChange
|
||||
import app.omnivore.omnivore.core.network.CreateHighlightParams
|
||||
import app.omnivore.omnivore.core.network.DeleteHighlightParams
|
||||
import app.omnivore.omnivore.core.network.MergeHighlightsParams
|
||||
import app.omnivore.omnivore.core.network.UpdateHighlightParams
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
|
||||
suspend fun DataService.createWebHighlight(jsonString: String, colorName: String?) {
|
||||
val createHighlightInput =
|
||||
Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = Highlight(
|
||||
type = "HIGHLIGHT",
|
||||
highlightId = createHighlightInput.id,
|
||||
shortId = createHighlightInput.shortId,
|
||||
quote = createHighlightInput.quote.getOrNull(),
|
||||
prefix = null,
|
||||
suffix = null,
|
||||
patch = createHighlightInput.patch.getOrNull(),
|
||||
annotation = createHighlightInput.annotation.getOrNull(),
|
||||
createdAt = null,
|
||||
updatedAt = null,
|
||||
createdByMe = false,
|
||||
color = colorName ?: createHighlightInput.color.getOrNull(),
|
||||
highlightPositionPercent = createHighlightInput.highlightPositionPercent.getOrNull()
|
||||
?: 0.0,
|
||||
highlightPositionAnchorIndex = createHighlightInput.highlightPositionAnchorIndex.getOrNull()
|
||||
?: 0
|
||||
)
|
||||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
|
||||
val highlightChange =
|
||||
saveHighlightChange(db.highlightChangesDao(), createHighlightInput.articleId, highlight)
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = createHighlightInput.id, savedItemId = createHighlightInput.articleId
|
||||
)
|
||||
|
||||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String {
|
||||
val shortId = NanoId.generate(size = 14)
|
||||
val createHighlightId = UUID.randomUUID().toString()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = Highlight(
|
||||
type = "NOTE",
|
||||
highlightId = createHighlightId,
|
||||
shortId = shortId,
|
||||
quote = null,
|
||||
prefix = null,
|
||||
suffix = null,
|
||||
patch = null,
|
||||
annotation = note,
|
||||
createdAt = null,
|
||||
updatedAt = null,
|
||||
createdByMe = true,
|
||||
color = null,
|
||||
highlightPositionAnchorIndex = 0,
|
||||
highlightPositionPercent = 0.0
|
||||
)
|
||||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
|
||||
val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = createHighlightId, savedItemId = savedItemId
|
||||
)
|
||||
|
||||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
|
||||
return createHighlightId
|
||||
}
|
||||
|
||||
suspend fun DataService.mergeWebHighlights(jsonString: String) {
|
||||
val mergeHighlightInput =
|
||||
Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput()
|
||||
Log.d("sync", "mergeHighlightInput: " + mergeHighlightInput.id + ": " + mergeHighlightInput)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = Highlight(
|
||||
type = "HIGHLIGHT",
|
||||
highlightId = mergeHighlightInput.id,
|
||||
shortId = mergeHighlightInput.shortId,
|
||||
quote = mergeHighlightInput.quote,
|
||||
prefix = null,
|
||||
suffix = null,
|
||||
patch = mergeHighlightInput.patch,
|
||||
annotation = mergeHighlightInput.annotation.getOrNull(),
|
||||
createdAt = null,
|
||||
updatedAt = null,
|
||||
createdByMe = false,
|
||||
color = mergeHighlightInput.color.getOrNull(),
|
||||
highlightPositionPercent = mergeHighlightInput.highlightPositionPercent.getOrNull()
|
||||
?: 0.0,
|
||||
highlightPositionAnchorIndex = mergeHighlightInput.highlightPositionAnchorIndex.getOrNull()
|
||||
?: 0
|
||||
)
|
||||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
||||
|
||||
val highlightChange = saveHighlightChange(
|
||||
db.highlightChangesDao(),
|
||||
mergeHighlightInput.articleId,
|
||||
highlight,
|
||||
html = mergeHighlightInput.html.getOrNull(),
|
||||
overlappingIDs = mergeHighlightInput.overlapHighlightIdList
|
||||
)
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = mergeHighlightInput.id, savedItemId = mergeHighlightInput.articleId
|
||||
)
|
||||
|
||||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
Log.d("sync", "Setting up highlight merge")
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.updateWebHighlight(jsonString: String) {
|
||||
val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java)
|
||||
|
||||
if (updateHighlightParams.highlightId == null || updateHighlightParams.libraryItemId == null) {
|
||||
Log.d("error", "ERROR INVALID HIGHLIGHT DATA")
|
||||
return
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight =
|
||||
db.highlightDao().findById(highlightId = updateHighlightParams.highlightId)
|
||||
?: return@withContext
|
||||
|
||||
highlight.annotation = updateHighlightParams.annotation
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
val highlightChange = saveHighlightChange(
|
||||
db.highlightChangesDao(), updateHighlightParams.libraryItemId, highlight
|
||||
)
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.deleteHighlightFromJSON(jsonString: String) {
|
||||
val deleteHighlightParams = Gson().fromJson(jsonString, DeleteHighlightParams::class.java)
|
||||
deleteHighlight(deleteHighlightParams.libraryItemId, deleteHighlightParams.highlightId)
|
||||
}
|
||||
|
||||
private suspend fun DataService.deleteHighlight(savedItemId: String, highlightID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = highlightID)
|
||||
|
||||
highlight?.let {
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
val highlightChange =
|
||||
saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
|
||||
performHighlightChange(highlightChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,14 @@
|
||||
package app.omnivore.omnivore.dataService
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.network.savedItem
|
||||
import app.omnivore.omnivore.core.network.savedItemUpdates
|
||||
import app.omnivore.omnivore.core.network.search
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
|
||||
suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult {
|
||||
val searchResult = networker.search(cursor = cursor, limit = 10, query = query)
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.dataService
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
|
||||
import java.security.SecureRandom
|
||||
@ -0,0 +1,34 @@
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||
import app.omnivore.omnivore.core.network.updateReadingProgress
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun DataService.updateWebReadingProgress(
|
||||
jsonString: String,
|
||||
savedItemDao: SavedItemDao
|
||||
) {
|
||||
val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
|
||||
val savedItemId = readingProgressParams.id ?: return
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = savedItemDao.findById(savedItemId) ?: return@withContext
|
||||
val updatedItem = savedItem.copy(
|
||||
readingProgress = readingProgressParams.readingProgressPercent ?: 0.0,
|
||||
readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0,
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
)
|
||||
savedItemDao.update(updatedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
updatedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
savedItemDao.update(updatedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
package app.omnivore.omnivore.dataService
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import app.omnivore.omnivore.networking.savedItemLabels
|
||||
import app.omnivore.omnivore.core.network.savedItemLabels
|
||||
|
||||
suspend fun DataService.syncLabels() {
|
||||
val fetchedLabels = networker.savedItemLabels()
|
||||
@ -0,0 +1,57 @@
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.network.archiveSavedItem
|
||||
import app.omnivore.omnivore.core.network.deleteSavedItem
|
||||
import app.omnivore.omnivore.core.network.unarchiveSavedItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun DataService.deleteSavedItem(itemID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.deleteSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
db.savedItemDao().deleteById(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.archiveSavedItem(itemID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext
|
||||
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
savedItem.isArchived = true
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.archiveSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.unarchiveSavedItem(itemID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext
|
||||
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
savedItem.isArchived = false
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.unarchiveSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,218 @@
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight
|
||||
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||
import app.omnivore.omnivore.core.network.createHighlight
|
||||
import app.omnivore.omnivore.core.network.deleteHighlights
|
||||
import app.omnivore.omnivore.core.network.deleteSavedItem
|
||||
import app.omnivore.omnivore.core.network.mergeHighlights
|
||||
import app.omnivore.omnivore.core.network.updateArchiveStatusSavedItem
|
||||
import app.omnivore.omnivore.core.network.updateHighlight
|
||||
import app.omnivore.omnivore.core.network.updateReadingProgress
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
suspend fun DataService.startSyncChannels() {
|
||||
Log.d("sync", "Starting sync channels")
|
||||
for (savedItem in savedItemSyncChannel) {
|
||||
syncSavedItem(savedItem)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.performHighlightChange(highlightChange: HighlightChange) {
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
if (syncHighlightChange(highlightChange)) {
|
||||
db.highlightChangesDao().deleteById(highlight.highlightId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun DataService.syncOfflineItemsWithServerIfNeeded() {
|
||||
val unSyncedSavedItems = db.savedItemDao().getUnSynced()
|
||||
val unSyncedHighlights = db.highlightChangesDao().getUnSynced()
|
||||
|
||||
for (savedItem in unSyncedSavedItems) {
|
||||
delay(250)
|
||||
savedItemSyncChannel.send(savedItem)
|
||||
}
|
||||
|
||||
for (change in unSyncedHighlights) {
|
||||
performHighlightChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncSavedItem(item: SavedItem) {
|
||||
suspend fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
item.serverSyncStatus = status.rawValue
|
||||
db.savedItemDao().update(item)
|
||||
}
|
||||
|
||||
when (item.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isDeletedOnServer = networker.deleteSavedItem(item.savedItemId)
|
||||
|
||||
if (isDeletedOnServer) {
|
||||
db.savedItemDao().deleteById(item.savedItemId)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
}
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isArchiveServerSynced = networker.updateArchiveStatusSavedItem(itemID = item.savedItemId, setAsArchived = item.isArchived)
|
||||
|
||||
val isReadingProgressSynced = networker.updateReadingProgress(
|
||||
ReadingProgressParams(
|
||||
id = item.savedItemId,
|
||||
force = item.contentReader == "PDF",
|
||||
readingProgressPercent = item.readingProgress,
|
||||
readingProgressAnchorIndex = item.readingProgressAnchor
|
||||
)
|
||||
)
|
||||
|
||||
if (isArchiveServerSynced && isReadingProgressSynced) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
}
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
// TODO: implement when we are able to create content on device
|
||||
// updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
// send update to server
|
||||
// update db
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncHighlightChange(highlightChange: HighlightChange): Boolean {
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
|
||||
fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
highlight.serverSyncStatus = status.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
|
||||
when (highlight.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId))
|
||||
|
||||
if (isDeletedOnServer) {
|
||||
db.highlightDao().deleteById(highlight.highlightId)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
return isDeletedOnServer != null
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isUpdatedOnServer = networker.updateHighlight(
|
||||
UpdateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
highlightId = highlight.highlightId,
|
||||
sharedAt = Optional.absent()
|
||||
)
|
||||
)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
return isUpdatedOnServer != null
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val input = CreateHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
type = Optional.presentIfNotNull(HighlightType.safeValueOf(highlight.type)),
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
patch = Optional.presentIfNotNull(highlight.patch),
|
||||
quote = Optional.presentIfNotNull(highlight.quote),
|
||||
)
|
||||
Log.d("sync", "Creating highlight from input: ${input}")
|
||||
val createResult = networker.createHighlight(
|
||||
input
|
||||
)
|
||||
if (createResult.newHighlight != null || createResult.alreadyExists) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
return true
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_MERGE.rawValue -> {
|
||||
Log.d("sync", "NEEDS MERGE: ${highlightChange}")
|
||||
|
||||
val mergeHighlightInput = MergeHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
color = Optional.presentIfNotNull(highlight.color),
|
||||
highlightPositionAnchorIndex = Optional.presentIfNotNull(highlight.highlightPositionAnchorIndex),
|
||||
highlightPositionPercent = Optional.presentIfNotNull(highlight.highlightPositionPercent),
|
||||
html = Optional.presentIfNotNull(highlightChange.html),
|
||||
overlapHighlightIdList = highlightChange.overlappingIDs ?: emptyList(),
|
||||
patch = highlight.patch ?: "",
|
||||
prefix = Optional.presentIfNotNull(highlight.prefix),
|
||||
quote = highlight.quote ?: "",
|
||||
suffix = Optional.presentIfNotNull(highlight.suffix)
|
||||
)
|
||||
|
||||
val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput)
|
||||
if (!isUpdatedOnServer) {
|
||||
Log.d("sync", "FAILED TO MERGE HIGHLIGHT")
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
||||
return false
|
||||
}
|
||||
|
||||
for (highlightID in mergeHighlightInput.overlapHighlightIdList) {
|
||||
Log.d("sync", "DELETING MERGED HIGHLIGHT: ${highlightID}")
|
||||
val deleteChange = HighlightChange(
|
||||
highlightId = highlightID,
|
||||
savedItemId = highlightChange.savedItemId,
|
||||
type = "",
|
||||
shortId = "",
|
||||
annotation = null,
|
||||
createdAt = null,
|
||||
patch = null,
|
||||
prefix = null,
|
||||
quote = null,
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue,
|
||||
html = null,
|
||||
suffix = null,
|
||||
updatedAt = null,
|
||||
color = null,
|
||||
highlightPositionPercent = null,
|
||||
highlightPositionAnchorIndex = null,
|
||||
overlappingIDs = null
|
||||
)
|
||||
performHighlightChange(deleteChange)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package app.omnivore.omnivore.core.data.model
|
||||
|
||||
data class LibraryQuery(
|
||||
val allowedArchiveStates: List<Int>,
|
||||
val sortKey: String,
|
||||
val requiredLabels: List<String>,
|
||||
val excludedLabels: List<String>,
|
||||
val allowedContentReaders: List<String>
|
||||
)
|
||||
@ -0,0 +1,10 @@
|
||||
package app.omnivore.omnivore.core.data.model
|
||||
|
||||
enum class ServerSyncStatus(val rawValue: Int) {
|
||||
IS_SYNCED(0),
|
||||
IS_SYNCING(1),
|
||||
NEEDS_DELETION(2),
|
||||
NEEDS_CREATION(3),
|
||||
NEEDS_UPDATE(4),
|
||||
NEEDS_MERGE(5)
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package app.omnivore.omnivore.core.data.repository
|
||||
|
||||
import app.omnivore.omnivore.core.data.model.LibraryQuery
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LibraryRepository {
|
||||
|
||||
fun getSavedItems(query: LibraryQuery): Flow<List<SavedItemWithLabelsAndHighlights>>
|
||||
|
||||
suspend fun updateReadingProgress(
|
||||
itemId: String,
|
||||
readingProgressPercentage: Double,
|
||||
readingProgressAnchorIndex: Int
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package app.omnivore.omnivore.core.data.repository.impl
|
||||
|
||||
import app.omnivore.omnivore.core.data.model.LibraryQuery
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.data.repository.LibraryRepository
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||
import app.omnivore.omnivore.core.network.updateReadingProgress
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class LibraryRepositoryImpl @Inject constructor(
|
||||
private val savedItemDao: SavedItemDao,
|
||||
private val networker: Networker
|
||||
): LibraryRepository {
|
||||
|
||||
override fun getSavedItems(query: LibraryQuery): Flow<List<SavedItemWithLabelsAndHighlights>> =
|
||||
savedItemDao.filteredLibraryData(
|
||||
query.allowedArchiveStates,
|
||||
query.sortKey,
|
||||
hasRequiredLabels = query.requiredLabels.size,
|
||||
hasExcludedLabels = query.excludedLabels.size,
|
||||
query.requiredLabels,
|
||||
query.excludedLabels,
|
||||
query.allowedContentReaders
|
||||
)
|
||||
|
||||
override suspend fun updateReadingProgress(
|
||||
itemId: String,
|
||||
readingProgressPercentage: Double,
|
||||
readingProgressAnchorIndex: Int
|
||||
) {
|
||||
|
||||
val jsonString = Gson().toJson(
|
||||
mapOf(
|
||||
"id" to itemId,
|
||||
"readingProgressPercent" to readingProgressPercentage,
|
||||
"readingProgressAnchorIndex" to readingProgressAnchorIndex,
|
||||
"force" to true
|
||||
)
|
||||
)
|
||||
|
||||
val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
|
||||
val savedItemId = readingProgressParams.id ?: return
|
||||
|
||||
|
||||
val savedItem = savedItemDao.findById(savedItemId)
|
||||
val updatedItem = savedItem?.copy(
|
||||
readingProgress = readingProgressParams.readingProgressPercent ?: 0.0,
|
||||
readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0,
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
)
|
||||
|
||||
updatedItem?.let { savedItemDao.update(updatedItem) }
|
||||
|
||||
val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
updatedItem?.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
updatedItem?.let { savedItemDao.update(updatedItem) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package app.omnivore.omnivore.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChangesDao
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRefDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRefDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabelDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlightsDao
|
||||
import app.omnivore.omnivore.core.database.entities.Viewer
|
||||
import app.omnivore.omnivore.core.database.entities.ViewerDao
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
Viewer::class,
|
||||
SavedItem::class,
|
||||
SavedItemLabel::class,
|
||||
Highlight::class,
|
||||
HighlightChange::class,
|
||||
SavedItemAndSavedItemLabelCrossRef::class,
|
||||
SavedItemAndHighlightCrossRef::class],
|
||||
version = 24,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class OmnivoreDatabase : RoomDatabase() {
|
||||
abstract fun viewerDao(): ViewerDao
|
||||
abstract fun savedItemDao(): SavedItemDao
|
||||
abstract fun highlightDao(): HighlightDao
|
||||
abstract fun highlightChangesDao(): HighlightChangesDao
|
||||
abstract fun savedItemLabelDao(): SavedItemLabelDao
|
||||
abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao
|
||||
abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao
|
||||
abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package app.omnivore.omnivore.core.database.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemQueryConstants
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SavedItemDao {
|
||||
|
||||
@Query("SELECT * FROM savedItem")
|
||||
fun getAll(): Flow<List<SavedItem>>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
|
||||
suspend fun findById(itemID: String): SavedItem?
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0")
|
||||
fun getUnSynced(): List<SavedItem>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE slug = :slug")
|
||||
fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights?
|
||||
|
||||
@Query("DELETE FROM savedItem WHERE savedItemId = :itemID")
|
||||
fun deleteById(itemID: String)
|
||||
|
||||
@Query("DELETE FROM savedItem WHERE savedItemId in (:itemIDs)")
|
||||
fun deleteByIds(itemIDs: List<String>)
|
||||
|
||||
@Update
|
||||
suspend fun update(savedItem: SavedItem)
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.savedItemId = :savedItemId " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId "
|
||||
)
|
||||
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.savedItemId = :savedItemId " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId "
|
||||
)
|
||||
suspend fun getById(savedItemId: String): SavedItemWithLabelsAndHighlights?
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.serverSyncStatus != 2 " +
|
||||
"AND SavedItem.isArchived IN (:allowedArchiveStates) " +
|
||||
"AND SavedItem.contentReader IN (:allowedContentReaders) " +
|
||||
"AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " +
|
||||
"AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId " +
|
||||
|
||||
"ORDER BY \n" +
|
||||
"CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" +
|
||||
"CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" +
|
||||
|
||||
"CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" +
|
||||
"CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC"
|
||||
)
|
||||
fun filteredLibraryData(
|
||||
allowedArchiveStates: List<Int>,
|
||||
sortKey: String,
|
||||
hasRequiredLabels: Int,
|
||||
hasExcludedLabels: Int,
|
||||
requiredLabels: List<String>,
|
||||
excludedLabels: List<String>,
|
||||
allowedContentReaders: List<String>
|
||||
): Flow<List<SavedItemWithLabelsAndHighlights>>
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package app.omnivore.omnivore.persistence.entities
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.room.*
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
@ -73,7 +73,20 @@ data class SavedItemWithLabelsAndHighlights(
|
||||
associateBy = Junction(SavedItemAndHighlightCrossRef::class)
|
||||
)
|
||||
val highlights: List<Highlight>
|
||||
)
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SavedItemWithLabelsAndHighlights
|
||||
|
||||
return savedItem.savedItemId == other.savedItem.savedItemId
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return savedItem.savedItemId.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface HighlightDao {
|
||||
@ -0,0 +1,125 @@
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
@Entity
|
||||
@TypeConverters(StringListTypeConverter::class)
|
||||
data class HighlightChange(
|
||||
@PrimaryKey val highlightId: String,
|
||||
val savedItemId: String,
|
||||
|
||||
val type: String,
|
||||
var annotation: String?,
|
||||
val createdAt: String?,
|
||||
val createdByMe: Boolean = true,
|
||||
val markedForDeletion: Boolean = false,
|
||||
var patch: String?,
|
||||
var prefix: String?,
|
||||
var quote: String?,
|
||||
var serverSyncStatus: Int = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
val html: String?,
|
||||
var shortId: String,
|
||||
val suffix: String?,
|
||||
val updatedAt: String?,
|
||||
val color: String?,
|
||||
val highlightPositionPercent: Double?,
|
||||
val highlightPositionAnchorIndex: Int?,
|
||||
val overlappingIDs: List<String>?
|
||||
)
|
||||
|
||||
class StringListTypeConverter {
|
||||
@TypeConverter
|
||||
fun listToString(data: List<String>?): String? {
|
||||
data?.let {
|
||||
return Gson().toJson(data)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToList(jsonString: String?): List<String>? {
|
||||
return if (jsonString.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val itemType = object : TypeToken<List<String>>() {}.type
|
||||
return Gson().fromJson<List<String>>(jsonString, itemType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun saveHighlightChange(
|
||||
dao: HighlightChangesDao,
|
||||
savedItemId: String,
|
||||
highlight: Highlight,
|
||||
html: String? = null,
|
||||
overlappingIDs: List<String>? = null): HighlightChange {
|
||||
|
||||
Log.d("sync", "saving highlight change: " + highlight.serverSyncStatus + ", " + highlight.type)
|
||||
val change = HighlightChange(
|
||||
savedItemId = savedItemId,
|
||||
highlightId = highlight.highlightId,
|
||||
type = highlight.type,
|
||||
shortId = highlight.shortId,
|
||||
quote = highlight.quote,
|
||||
prefix = highlight.prefix,
|
||||
suffix = highlight.suffix,
|
||||
patch = highlight.patch,
|
||||
html = html,
|
||||
annotation = highlight.annotation,
|
||||
createdAt = highlight.createdAt,
|
||||
updatedAt = highlight.updatedAt,
|
||||
createdByMe = highlight.createdByMe,
|
||||
color =highlight.color,
|
||||
highlightPositionPercent = highlight.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = highlight.highlightPositionAnchorIndex,
|
||||
serverSyncStatus = highlight.serverSyncStatus,
|
||||
overlappingIDs = overlappingIDs
|
||||
)
|
||||
dao.insertAll(listOf(change))
|
||||
return change
|
||||
}
|
||||
|
||||
fun highlightChangeToHighlight(change: HighlightChange): Highlight {
|
||||
return Highlight(
|
||||
highlightId = change.highlightId,
|
||||
type = change.type,
|
||||
shortId = change.shortId,
|
||||
quote = change.quote,
|
||||
prefix = change.prefix,
|
||||
suffix = change.suffix,
|
||||
patch = change.patch,
|
||||
annotation = change.annotation,
|
||||
createdAt = change.createdAt,
|
||||
updatedAt = change.updatedAt,
|
||||
createdByMe = change.createdByMe,
|
||||
color = change.color,
|
||||
highlightPositionPercent = change.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = change.highlightPositionAnchorIndex,
|
||||
serverSyncStatus = change.serverSyncStatus
|
||||
)
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface HighlightChangesDao {
|
||||
@Query("SELECT * FROM highlightChange WHERE serverSyncStatus != 0 ORDER BY updatedAt ASC")
|
||||
fun getUnSynced(): List<HighlightChange>
|
||||
|
||||
@Query("DELETE FROM highlightChange WHERE highlightId = :highlightId")
|
||||
fun deleteById(highlightId: String)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<HighlightChange>)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.persistence.entities
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.persistence.entities
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.persistence.entities
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.persistence.entities
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@ -0,0 +1,165 @@
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Transaction
|
||||
|
||||
@Entity
|
||||
data class SavedItem(
|
||||
@PrimaryKey val savedItemId: String,
|
||||
val title: String,
|
||||
val createdAt: String,
|
||||
val savedAt: String,
|
||||
val readAt: String?,
|
||||
val updatedAt: String?,
|
||||
var readingProgress: Double,
|
||||
var readingProgressAnchor: Int,
|
||||
val imageURLString: String?,
|
||||
val pageURLString: String,
|
||||
val descriptionText: String?,
|
||||
val publisherURLString: String?,
|
||||
val siteName: String?,
|
||||
val author: String?,
|
||||
val publishDate: String?,
|
||||
val slug: String,
|
||||
var isArchived: Boolean,
|
||||
val contentReader: String? = null,
|
||||
val content: String? = null,
|
||||
val createdId: String? = null,
|
||||
val htmlContent: String? = null,
|
||||
val language: String? = null,
|
||||
val listenPositionIndex: Int? = null,
|
||||
val listenPositionOffset: Double? = null,
|
||||
val listenPositionTime: Double? = null,
|
||||
val localPDF: String? = null,
|
||||
val onDeviceImageURLString: String? = null,
|
||||
val originalHtml: String? = null,
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) val pdfData: ByteArray? = null,
|
||||
var serverSyncStatus: Int = 0,
|
||||
val tempPDFURL: String? = null,
|
||||
val wordsCount: Int? = null,
|
||||
val localPDFPath: String? = null
|
||||
|
||||
// hasMany highlights
|
||||
// hasMany labels
|
||||
// has Many recommendations (rec has one savedItem)
|
||||
) {
|
||||
fun publisherDisplayName(): String? {
|
||||
return publisherURLString?.toUri()?.host
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SavedItem
|
||||
|
||||
return savedItemId == other.savedItemId
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return savedItemId.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
data class TypeaheadCardData(
|
||||
val savedItemId: String,
|
||||
val slug: String,
|
||||
val title: String,
|
||||
val isArchived: Boolean,
|
||||
)
|
||||
|
||||
@Dao
|
||||
abstract class SavedItemWithLabelsAndHighlightsDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertSavedItems(items: List<SavedItem>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertLabelCrossRefs(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertLabels(items: List<SavedItemLabel>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertHighlights(items: List<Highlight>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertHighlightCrossRefs(items: List<SavedItemAndHighlightCrossRef>)
|
||||
|
||||
@Transaction
|
||||
open fun insertAll(savedItems: List<SavedItemWithLabelsAndHighlights>) {
|
||||
insertSavedItems(savedItems.map { it.savedItem })
|
||||
|
||||
val labels: MutableList<SavedItemLabel> = mutableListOf()
|
||||
val highlights: MutableList<Highlight> = mutableListOf()
|
||||
|
||||
val labelCrossRefs: MutableList<SavedItemAndSavedItemLabelCrossRef> = mutableListOf()
|
||||
val highlightCrossRefs: MutableList<SavedItemAndHighlightCrossRef> = mutableListOf()
|
||||
|
||||
for (searchItem in savedItems) {
|
||||
labels.addAll(searchItem.labels)
|
||||
highlights.addAll(searchItem.highlights)
|
||||
|
||||
val newLabelCrossRefs = searchItem.labels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(
|
||||
savedItemLabelId = it.savedItemLabelId,
|
||||
savedItemId = searchItem.savedItem.savedItemId
|
||||
)
|
||||
}
|
||||
|
||||
val newHighlightCrossRefs = searchItem.highlights.map {
|
||||
SavedItemAndHighlightCrossRef(
|
||||
highlightId = it.highlightId,
|
||||
savedItemId = searchItem.savedItem.savedItemId
|
||||
)
|
||||
}
|
||||
|
||||
labelCrossRefs.addAll(newLabelCrossRefs)
|
||||
highlightCrossRefs.addAll(newHighlightCrossRefs)
|
||||
}
|
||||
|
||||
insertLabels(labels)
|
||||
insertLabelCrossRefs(labelCrossRefs)
|
||||
|
||||
insertHighlights(highlights)
|
||||
insertHighlightCrossRefs(highlightCrossRefs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
object SavedItemQueryConstants {
|
||||
const val columns =
|
||||
"savedItemId, slug, publisherURLString, title, author, descriptionText, imageURLString, isArchived, pageURLString, contentReader, savedAt, readingProgress, wordsCount"
|
||||
const val libraryColumns = "SavedItem.savedItemId, " +
|
||||
"SavedItem.slug, " +
|
||||
"SavedItem.createdAt, " +
|
||||
|
||||
"SavedItem.publisherURLString, " +
|
||||
"SavedItem.title, " +
|
||||
"SavedItem.author, " +
|
||||
"SavedItem.descriptionText, " +
|
||||
"SavedItem.imageURLString, " +
|
||||
"SavedItem.isArchived, " +
|
||||
"SavedItem.pageURLString, " +
|
||||
"SavedItem.contentReader, " +
|
||||
"SavedItem.savedAt, " +
|
||||
"SavedItem.readingProgress, " +
|
||||
"SavedItem.readingProgressAnchor, " +
|
||||
"SavedItem.serverSyncStatus, " +
|
||||
|
||||
"SavedItem.wordsCount, " +
|
||||
"SavedItemLabel.savedItemLabelId, " +
|
||||
"SavedItemLabel.name, " +
|
||||
"SavedItemLabel.color, " +
|
||||
"Highlight.highlightId, " +
|
||||
"Highlight.shortId, " +
|
||||
"Highlight.createdByMe "
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
|
||||
@Entity
|
||||
data class SavedItemLabel(
|
||||
@PrimaryKey val savedItemLabelId: String,
|
||||
val name: String,
|
||||
val color: String,
|
||||
val createdAt: String?,
|
||||
val labelDescription: String?,
|
||||
val serverSyncStatus: Int = 0
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface SavedItemLabelDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemLabel>)
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
|
||||
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
|
||||
|
||||
@Transaction
|
||||
@Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId")
|
||||
fun updateTempLabel(
|
||||
tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED
|
||||
)
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC")
|
||||
fun namedLabels(names: List<String>): List<SavedItemLabel>
|
||||
}
|
||||
|
||||
@Entity(
|
||||
primaryKeys = ["savedItemLabelId", "savedItemId"], foreignKeys = [ForeignKey(
|
||||
entity = SavedItem::class,
|
||||
parentColumns = arrayOf("savedItemId"),
|
||||
childColumns = arrayOf("savedItemId"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
), ForeignKey(
|
||||
entity = SavedItemLabel::class,
|
||||
parentColumns = arrayOf("savedItemLabelId"),
|
||||
childColumns = arrayOf("savedItemLabelId")
|
||||
)]
|
||||
)
|
||||
data class SavedItemAndSavedItemLabelCrossRef(
|
||||
val savedItemLabelId: String, val savedItemId: String
|
||||
)
|
||||
|
||||
|
||||
@Dao
|
||||
interface SavedItemAndSavedItemLabelCrossRefDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||
|
||||
@Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId")
|
||||
fun deleteRefsBySavedItemId(savedItemId: String)
|
||||
}
|
||||
|
||||
// has many highlights
|
||||
// has many savedItems
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.persistence.entities
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@ -1,7 +1,6 @@
|
||||
package app.omnivore.omnivore.persistence.entities
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.room.*
|
||||
import app.omnivore.omnivore.persistence.BaseDao
|
||||
|
||||
@Entity
|
||||
data class Viewer(
|
||||
@ -1,9 +1,11 @@
|
||||
package app.omnivore.omnivore
|
||||
package app.omnivore.omnivore.core.datastore
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.*
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import app.omnivore.omnivore.utils.Constants
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -1,15 +1,16 @@
|
||||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.graphql.generated.CreateHighlightMutation
|
||||
import app.omnivore.omnivore.graphql.generated.DeleteHighlightMutation
|
||||
import app.omnivore.omnivore.graphql.generated.MergeHighlightMutation
|
||||
import app.omnivore.omnivore.graphql.generated.UpdateHighlightMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightErrorCode
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
|
||||
@ -39,6 +40,7 @@ data class CreateHighlightParams(
|
||||
|
||||
data class UpdateHighlightParams(
|
||||
val highlightId: String?,
|
||||
val libraryItemId: String?,
|
||||
val `annotation`: String?,
|
||||
val sharedAt: String?,
|
||||
) {
|
||||
@ -73,9 +75,10 @@ data class MergeHighlightsParams(
|
||||
}
|
||||
|
||||
data class DeleteHighlightParams(
|
||||
val highlightId: String?
|
||||
val highlightId: String,
|
||||
val libraryItemId: String
|
||||
) {
|
||||
fun asIdList() = listOf(highlightId ?: "")
|
||||
fun asIdList() = listOf(highlightId)
|
||||
}
|
||||
|
||||
suspend fun Networker.deleteHighlight(jsonString: String): Boolean {
|
||||
@ -107,7 +110,6 @@ suspend fun Networker.updateWebHighlight(jsonString: String): Boolean {
|
||||
suspend fun Networker.updateHighlight(input: UpdateHighlightInput): Boolean {
|
||||
return try {
|
||||
val result = authenticatedApolloClient().mutation(UpdateHighlightMutation(input)).execute()
|
||||
Log.d("Network", "update highlight result: $result")
|
||||
result.data?.updateHighlight?.onUpdateHighlightSuccess?.highlight != null
|
||||
} catch (e: java.lang.Exception) {
|
||||
false
|
||||
@ -134,18 +136,22 @@ suspend fun Networker.createWebHighlight(jsonString: String): Boolean {
|
||||
return createHighlight(input) != null
|
||||
}
|
||||
|
||||
suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
|
||||
Log.d("Loggo", "created highlight input: $input")
|
||||
data class CreateHighlightResult(
|
||||
val failedToCreate: Boolean,
|
||||
val alreadyExists: Boolean,
|
||||
val newHighlight: Highlight?
|
||||
)
|
||||
|
||||
suspend fun Networker.createHighlight(input: CreateHighlightInput): CreateHighlightResult {
|
||||
try {
|
||||
val result = authenticatedApolloClient().mutation(CreateHighlightMutation(input)).execute()
|
||||
Log.d("Loggo", "result: ${result.data}")
|
||||
|
||||
|
||||
val createdHighlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight
|
||||
|
||||
if (createdHighlight != null) {
|
||||
return Highlight(
|
||||
return CreateHighlightResult(
|
||||
failedToCreate = false,
|
||||
alreadyExists = false,
|
||||
newHighlight = Highlight(
|
||||
type = createdHighlight.highlightFields.type.toString(),
|
||||
highlightId = createdHighlight.highlightFields.id,
|
||||
shortId = createdHighlight.highlightFields.shortId,
|
||||
@ -161,10 +167,22 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
|
||||
highlightPositionPercent = createdHighlight.highlightFields.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = createdHighlight.highlightFields.highlightPositionAnchorIndex
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
if (result.data?.createHighlight?.onCreateHighlightError?.errorCodes?.first() == CreateHighlightErrorCode.ALREADY_EXISTS) {
|
||||
return CreateHighlightResult(
|
||||
failedToCreate = false,
|
||||
alreadyExists = true,
|
||||
newHighlight = null
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
return null
|
||||
Log.d("sync", "error creating highlight: " +e)
|
||||
}
|
||||
return CreateHighlightResult(
|
||||
failedToCreate = true,
|
||||
alreadyExists = false,
|
||||
newHighlight = null
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.utils.Constants
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import javax.inject.Inject
|
||||
|
||||
class Networker @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
) {
|
||||
suspend fun baseUrl() =
|
||||
datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL
|
||||
|
||||
private suspend fun serverUrl() = "${baseUrl()}/api/graphql"
|
||||
private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: ""
|
||||
|
||||
suspend fun authenticatedApolloClient() = ApolloClient.Builder().serverUrl(serverUrl())
|
||||
.addHttpHeader("Authorization", value = authToken()).build()
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package app.omnivore.omnivore
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.SaveArticleReadingProgressMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.SaveArticleReadingProgressInput
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.CreateLabelMutation
|
||||
import app.omnivore.omnivore.graphql.generated.SetLabelsMutation
|
||||
@ -1,7 +1,7 @@
|
||||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.GetLabelsQuery
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
|
||||
|
||||
suspend fun Networker.savedItemLabels(): List<SavedItemLabel> {
|
||||
@ -0,0 +1,65 @@
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import app.omnivore.omnivore.graphql.generated.SaveUrlMutation
|
||||
import app.omnivore.omnivore.graphql.generated.SetBookmarkArticleMutation
|
||||
import app.omnivore.omnivore.graphql.generated.SetLinkArchivedMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.ArchiveLinkInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.SaveUrlInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.SetBookmarkArticleInput
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import java.util.TimeZone
|
||||
import java.util.UUID
|
||||
|
||||
suspend fun Networker.deleteSavedItem(itemID: String): Boolean {
|
||||
return try {
|
||||
val input = SetBookmarkArticleInput(itemID, false)
|
||||
val result =
|
||||
authenticatedApolloClient().mutation(SetBookmarkArticleMutation(input)).execute()
|
||||
result.data?.setBookmarkArticle?.onSetBookmarkArticleSuccess?.bookmarkedArticle?.id != null
|
||||
} catch (e: java.lang.Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Networker.archiveSavedItem(itemID: String): Boolean {
|
||||
return updateArchiveStatusSavedItem(itemID, true)
|
||||
}
|
||||
|
||||
suspend fun Networker.unarchiveSavedItem(itemID: String): Boolean {
|
||||
return updateArchiveStatusSavedItem(itemID, false)
|
||||
}
|
||||
|
||||
suspend fun Networker.updateArchiveStatusSavedItem(
|
||||
itemID: String,
|
||||
setAsArchived: Boolean
|
||||
): Boolean {
|
||||
return try {
|
||||
val input = ArchiveLinkInput(setAsArchived, itemID)
|
||||
val result = authenticatedApolloClient().mutation(SetLinkArchivedMutation(input)).execute()
|
||||
result.data?.setLinkArchived?.onArchiveLinkSuccess?.linkId != null
|
||||
} catch (e: java.lang.Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Networker.saveUrl(url: Uri): Boolean {
|
||||
return try {
|
||||
val clientRequestId = UUID.randomUUID().toString()
|
||||
// get locale and timezone from device
|
||||
val timezone = TimeZone.getDefault().id
|
||||
val locale = Locale.current.toLanguageTag()
|
||||
val input = SaveUrlInput(
|
||||
url = url.toString(),
|
||||
clientRequestId = clientRequestId,
|
||||
source = "android",
|
||||
timezone = Optional.present(timezone),
|
||||
locale = Optional.present(locale)
|
||||
)
|
||||
val result = authenticatedApolloClient().mutation(SaveUrlMutation(input)).execute()
|
||||
result.data?.saveUrl?.onSaveSuccess?.url != null
|
||||
} catch (e: java.lang.Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.graphql.generated.GetArticleQuery
|
||||
import app.omnivore.omnivore.graphql.generated.type.ContentReader
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.nio.file.Files
|
||||
@ -1,8 +1,7 @@
|
||||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.UpdatesSinceQuery
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateReason
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
data class SavedItemUpdatesQueryResponse(
|
||||
@ -20,6 +19,7 @@ suspend fun Networker.savedItemUpdates(
|
||||
try {
|
||||
val result = authenticatedApolloClient().query(
|
||||
UpdatesSinceQuery(
|
||||
folder = Optional.presentIfNotNull("all"),
|
||||
after = Optional.presentIfNotNull(cursor),
|
||||
first = Optional.presentIfNotNull(limit),
|
||||
since = since
|
||||
@ -1,8 +1,10 @@
|
||||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
data class LibrarySearchQueryResponse(
|
||||
@ -1,7 +1,7 @@
|
||||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery
|
||||
import app.omnivore.omnivore.persistence.entities.TypeaheadCardData
|
||||
import app.omnivore.omnivore.core.database.entities.TypeaheadCardData
|
||||
|
||||
data class SearchQueryResponse(
|
||||
val cursor: String?,
|
||||
@ -1,7 +1,7 @@
|
||||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.ViewerQuery
|
||||
import app.omnivore.omnivore.persistence.entities.Viewer
|
||||
import app.omnivore.omnivore.core.database.entities.Viewer
|
||||
|
||||
suspend fun Networker.viewer(): Viewer? {
|
||||
try {
|
||||
@ -0,0 +1,33 @@
|
||||
package app.omnivore.omnivore.core.ui
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun LinkIcon(
|
||||
label: String,
|
||||
icon: ImageVector,
|
||||
url: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
IconButton(
|
||||
modifier = modifier.padding(4.dp),
|
||||
onClick = { uriHandler.openUri(url) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.AppDatabase
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import javax.inject.Inject
|
||||
|
||||
class DataService @Inject constructor(
|
||||
context: Context,
|
||||
val networker: Networker
|
||||
) {
|
||||
val savedItemSyncChannel = Channel<SavedItem>(capacity = Channel.UNLIMITED)
|
||||
val highlightSyncChannel = Channel<Highlight>(capacity = Channel.UNLIMITED)
|
||||
|
||||
val db = Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java, "omnivore-database"
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
startSyncChannels()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearDatabase() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
db.clearAllTables()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,176 +0,0 @@
|
||||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemAndHighlightCrossRef
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
suspend fun DataService.createWebHighlight(jsonString: String, colorName: String?) {
|
||||
val createHighlightInput = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = Highlight(
|
||||
type = "HIGHLIGHT",
|
||||
highlightId = createHighlightInput.id,
|
||||
shortId = createHighlightInput.shortId,
|
||||
quote = createHighlightInput.quote.getOrNull(),
|
||||
prefix = null,
|
||||
suffix = null,
|
||||
patch = createHighlightInput.patch.getOrNull(),
|
||||
annotation = createHighlightInput.annotation.getOrNull(),
|
||||
createdAt = null,
|
||||
updatedAt = null,
|
||||
createdByMe = false,
|
||||
color = colorName ?: createHighlightInput.color.getOrNull(),
|
||||
highlightPositionPercent = createHighlightInput.highlightPositionPercent.getOrNull() ?: 0.0,
|
||||
highlightPositionAnchorIndex = createHighlightInput.highlightPositionAnchorIndex.getOrNull() ?: 0
|
||||
)
|
||||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = createHighlightInput.id,
|
||||
savedItemId = createHighlightInput.articleId
|
||||
)
|
||||
|
||||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
val newHighlight = networker.createHighlight(createHighlightInput)
|
||||
|
||||
newHighlight?.let {
|
||||
db.highlightDao().update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String {
|
||||
val shortId = NanoId.generate(size=14)
|
||||
val createHighlightId = UUID.randomUUID().toString()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = Highlight(
|
||||
type = "NOTE",
|
||||
highlightId = createHighlightId,
|
||||
shortId = shortId,
|
||||
quote = null,
|
||||
prefix = null,
|
||||
suffix = null,
|
||||
patch =null,
|
||||
annotation = note,
|
||||
createdAt = null,
|
||||
updatedAt = null,
|
||||
createdByMe = true,
|
||||
color = null,
|
||||
highlightPositionAnchorIndex = 0,
|
||||
highlightPositionPercent = 0.0
|
||||
)
|
||||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = createHighlightId,
|
||||
savedItemId = savedItemId
|
||||
)
|
||||
|
||||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
val newHighlight = networker.createHighlight(input = CreateHighlightParams(
|
||||
type = HighlightType.NOTE,
|
||||
articleId = savedItemId,
|
||||
id = createHighlightId,
|
||||
shortId = shortId,
|
||||
quote = null,
|
||||
patch = null,
|
||||
annotation = note,
|
||||
highlightPositionAnchorIndex = 0,
|
||||
highlightPositionPercent = 0.0
|
||||
).asCreateHighlightInput())
|
||||
|
||||
newHighlight?.let {
|
||||
db.highlightDao().update(it)
|
||||
}
|
||||
}
|
||||
|
||||
return createHighlightId
|
||||
}
|
||||
|
||||
suspend fun DataService.mergeWebHighlights(jsonString: String) {
|
||||
val mergeHighlightInput = Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = mergeHighlightInput.id) ?: return@withContext
|
||||
highlight.shortId = mergeHighlightInput.shortId
|
||||
highlight.quote = mergeHighlightInput.quote
|
||||
highlight.patch = mergeHighlightInput.patch
|
||||
highlight.prefix = mergeHighlightInput.prefix.getOrNull()
|
||||
highlight.annotation = mergeHighlightInput.annotation.getOrNull()
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
|
||||
for (highlightID in mergeHighlightInput.overlapHighlightIdList) {
|
||||
deleteHighlight(highlightID)
|
||||
}
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = mergeHighlightInput.id,
|
||||
savedItemId = mergeHighlightInput.articleId
|
||||
)
|
||||
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
highlight.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.updateWebHighlight(jsonString: String) {
|
||||
val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java).asUpdateHighlightInput()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = updateHighlightParams.highlightId) ?: return@withContext
|
||||
|
||||
highlight.annotation = updateHighlightParams.annotation.getOrNull()
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
val isUpdatedOnServer = networker.updateHighlight(updateHighlightParams)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
highlight.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.deleteHighlights(jsonString: String) {
|
||||
val highlightIDs = Gson().fromJson(jsonString, DeleteHighlightParams::class.java).asIdList()
|
||||
|
||||
for (highlightID in highlightIDs) {
|
||||
deleteHighlight(highlightID)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.deleteHighlight(highlightID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = highlightID) ?: return@withContext
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
val isUpdatedOnServer = networker.deleteHighlights(listOf(highlightID))
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
db.highlightDao().deleteById(highlightId = highlightID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.ReadingProgressParams
|
||||
import app.omnivore.omnivore.networking.updateReadingProgress
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun DataService.updateWebReadingProgress(jsonString: String) {
|
||||
val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
|
||||
val savedItemId = readingProgressParams.id ?: return
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(savedItemId) ?: return@withContext
|
||||
savedItem.readingProgress = readingProgressParams.readingProgressPercent ?: 0.0
|
||||
savedItem.readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.archiveSavedItem
|
||||
import app.omnivore.omnivore.networking.deleteSavedItem
|
||||
import app.omnivore.omnivore.networking.unarchiveSavedItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun DataService.deleteSavedItem(itemID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.deleteSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
db.savedItemDao().deleteById(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.archiveSavedItem(itemID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext
|
||||
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
savedItem.isArchived = true
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.archiveSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.unarchiveSavedItem(itemID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext
|
||||
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
savedItem.isArchived = false
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.unarchiveSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,145 +0,0 @@
|
||||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
suspend fun DataService.startSyncChannels() {
|
||||
for (savedItem in savedItemSyncChannel) {
|
||||
syncSavedItem(savedItem)
|
||||
}
|
||||
|
||||
for (highlight in highlightSyncChannel) {
|
||||
syncHighlight(highlight)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.syncOfflineItemsWithServerIfNeeded() {
|
||||
val unSyncedSavedItems = db.savedItemDao().getUnSynced()
|
||||
val unSyncedHighlights = db.highlightDao().getUnSynced()
|
||||
|
||||
for (savedItem in unSyncedSavedItems) {
|
||||
delay(250)
|
||||
savedItemSyncChannel.send(savedItem)
|
||||
}
|
||||
|
||||
for (highlight in unSyncedHighlights) {
|
||||
delay(250)
|
||||
highlightSyncChannel.send(highlight)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncSavedItem(item: SavedItem) {
|
||||
fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
item.serverSyncStatus = status.rawValue
|
||||
db.savedItemDao().update(item)
|
||||
}
|
||||
|
||||
when (item.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isDeletedOnServer = networker.deleteSavedItem(item.savedItemId)
|
||||
|
||||
if (isDeletedOnServer) {
|
||||
db.savedItemDao().deleteById(item.savedItemId)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
}
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isArchiveServerSynced = networker.updateArchiveStatusSavedItem(itemID = item.savedItemId, setAsArchived = item.isArchived)
|
||||
|
||||
val isReadingProgressSynced = networker.updateReadingProgress(
|
||||
ReadingProgressParams(
|
||||
id = item.savedItemId,
|
||||
force = item.contentReader == "PDF",
|
||||
readingProgressPercent = item.readingProgress,
|
||||
readingProgressAnchorIndex = item.readingProgressAnchor
|
||||
)
|
||||
)
|
||||
|
||||
if (isArchiveServerSynced && isReadingProgressSynced) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
}
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
// TODO: implement when we are able to create content on device
|
||||
// updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
// send update to server
|
||||
// update db
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncHighlight(highlight: Highlight) {
|
||||
fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
highlight.serverSyncStatus = status.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
|
||||
when (highlight.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId))
|
||||
|
||||
if (isDeletedOnServer) {
|
||||
db.highlightDao().deleteById(highlight.highlightId)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
}
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isUpdatedOnServer = networker.updateHighlight(
|
||||
UpdateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
highlightId = highlight.highlightId,
|
||||
sharedAt = Optional.absent()
|
||||
)
|
||||
)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
}
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val savedItemID = db.savedItemAndHighlightCrossRefDao()
|
||||
.associatedSavedItemID(highlightId = highlight.highlightId)
|
||||
|
||||
val isCreatedOnServer = networker.createHighlight(
|
||||
CreateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
articleId = savedItemID ?: "",
|
||||
id = highlight.highlightId,
|
||||
patch = Optional.presentIfNotNull(highlight.patch),
|
||||
quote = Optional.presentIfNotNull(highlight.quote),
|
||||
shortId = highlight.shortId
|
||||
)
|
||||
)
|
||||
|
||||
if (isCreatedOnServer != null) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,12 @@
|
||||
package app.omnivore.omnivore
|
||||
package app.omnivore.omnivore.di
|
||||
|
||||
import android.content.Context
|
||||
import app.omnivore.omnivore.dataService.DataService
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.database.OmnivoreDatabase
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.datastore.OmnivoreDatastore
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@ -31,7 +35,8 @@ object AppModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideDataService(
|
||||
@ApplicationContext app: Context,
|
||||
networker: Networker
|
||||
) = DataService(app, networker)
|
||||
networker: Networker,
|
||||
omnivoreDatabase: OmnivoreDatabase
|
||||
) = DataService(networker, omnivoreDatabase)
|
||||
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package app.omnivore.omnivore.di
|
||||
|
||||
import app.omnivore.omnivore.core.database.OmnivoreDatabase
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DaosModule {
|
||||
|
||||
@Provides
|
||||
fun providesSavedItemDao(
|
||||
database: OmnivoreDatabase,
|
||||
): SavedItemDao = database.savedItemDao()
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package app.omnivore.omnivore.di
|
||||
|
||||
import app.omnivore.omnivore.core.data.repository.LibraryRepository
|
||||
import app.omnivore.omnivore.core.data.repository.impl.LibraryRepositoryImpl
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface DataModule {
|
||||
|
||||
@Binds
|
||||
fun bindsLibraryRepository(
|
||||
libraryRepository: LibraryRepositoryImpl,
|
||||
): LibraryRepository
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package app.omnivore.omnivore.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import app.omnivore.omnivore.core.database.OmnivoreDatabase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesOmnivoreDatabase(
|
||||
@ApplicationContext context: Context,
|
||||
): OmnivoreDatabase = Room.databaseBuilder(
|
||||
context,
|
||||
OmnivoreDatabase::class.java,
|
||||
"omnivore-database",
|
||||
).build()
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
package app.omnivore.omnivore.ui
|
||||
package app.omnivore.omnivore.feature
|
||||
|
||||
import app.omnivore.omnivore.dataService.DataService
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import app.omnivore.omnivore.networking.updateLabelsForSavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.updateLabelsForSavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui
|
||||
package app.omnivore.omnivore.feature
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
@ -17,7 +17,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import app.omnivore.omnivore.AppleConstants
|
||||
import app.omnivore.omnivore.utils.AppleConstants
|
||||
import app.omnivore.omnivore.R
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
@ -0,0 +1,39 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
|
||||
|
||||
object AuthUtils {
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun Modifier.autofill(
|
||||
autofillTypes: List<AutofillType>,
|
||||
onFill: ((String) -> Unit),
|
||||
) = composed {
|
||||
val autofill = LocalAutofill.current
|
||||
val autofillNode = AutofillNode(onFill = onFill, autofillTypes = autofillTypes)
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
this
|
||||
.onGloballyPositioned {
|
||||
autofillNode.boundingBox = it.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
@ -10,7 +10,9 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@ -25,6 +27,7 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.auth.AuthUtils.autofill
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
@ -86,6 +89,7 @@ fun EmailLoginView(viewModel: LoginViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginFields(
|
||||
email: String,
|
||||
@ -105,6 +109,12 @@ fun LoginFields(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.EmailAddress,
|
||||
),
|
||||
onFill = { onEmailChange(it) }
|
||||
),
|
||||
value = email,
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_email)) },
|
||||
@ -112,11 +122,17 @@ fun LoginFields(
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Email,
|
||||
),
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.Password,
|
||||
),
|
||||
onFill = { onPasswordChange(it) }
|
||||
),
|
||||
value = password,
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_password)) },
|
||||
@ -129,21 +145,22 @@ fun LoginFields(
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
Button(onClick = {
|
||||
if (email.isNotBlank() && password.isNotBlank()) {
|
||||
onLoginClick()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.email_login_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D),
|
||||
containerColor = Color(0xffffd234)
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
if (email.isNotBlank() && password.isNotBlank()) {
|
||||
onLoginClick()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.email_login_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D),
|
||||
containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_login_action_login),
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
@ -14,7 +14,9 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@ -27,6 +29,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.auth.AuthUtils.autofill
|
||||
|
||||
@Composable
|
||||
fun EmailSignUpView(viewModel: LoginViewModel) {
|
||||
@ -140,6 +143,7 @@ fun EmailSignUpForm(viewModel: LoginViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun EmailSignUpFields(
|
||||
email: String,
|
||||
@ -165,6 +169,12 @@ fun EmailSignUpFields(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.EmailAddress,
|
||||
),
|
||||
onFill = { onEmailChange(it) }
|
||||
),
|
||||
value = email,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_email)) },
|
||||
@ -174,6 +184,12 @@ fun EmailSignUpFields(
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.Password,
|
||||
),
|
||||
onFill = { onPasswordChange(it) }
|
||||
),
|
||||
value = password,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_password)) },
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
@ -7,11 +7,26 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import app.omnivore.omnivore.*
|
||||
import app.omnivore.omnivore.dataService.DataService
|
||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.network.AuthProviderLoginSubmit
|
||||
import app.omnivore.omnivore.core.network.CreateAccountParams
|
||||
import app.omnivore.omnivore.core.network.CreateAccountSubmit
|
||||
import app.omnivore.omnivore.core.network.CreateEmailAccountSubmit
|
||||
import app.omnivore.omnivore.core.network.EmailLoginCredentials
|
||||
import app.omnivore.omnivore.core.network.EmailLoginSubmit
|
||||
import app.omnivore.omnivore.core.network.EmailSignUpParams
|
||||
import app.omnivore.omnivore.graphql.generated.ValidateUsernameQuery
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import app.omnivore.omnivore.networking.viewer
|
||||
import app.omnivore.omnivore.ui.ResourceProvider
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.PendingUserSubmit
|
||||
import app.omnivore.omnivore.core.network.RetrofitHelper
|
||||
import app.omnivore.omnivore.core.network.SignInParams
|
||||
import app.omnivore.omnivore.core.network.UserProfile
|
||||
import app.omnivore.omnivore.core.network.viewer
|
||||
import app.omnivore.omnivore.feature.ResourceProvider
|
||||
import app.omnivore.omnivore.utils.Constants
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
@ -1,13 +1,8 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
@ -19,22 +14,16 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@ -0,0 +1,172 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun WelcomeScreen(viewModel: LoginViewModel) {
|
||||
OmnivoreTheme(darkTheme = false) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = Color(0xFFFCEBA8)
|
||||
) {
|
||||
WelcomeScreenContent(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
fun WelcomeScreenContent(viewModel: LoginViewModel) {
|
||||
val registrationState: RegistrationState by viewModel.registrationStateLiveData.observeAsState(
|
||||
RegistrationState.SocialLogin
|
||||
)
|
||||
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_omnivore_name_logo),
|
||||
contentDescription = "Omnivore Icon with Name"
|
||||
)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
|
||||
when (registrationState) {
|
||||
RegistrationState.EmailSignIn -> {
|
||||
EmailLoginView(viewModel = viewModel)
|
||||
}
|
||||
|
||||
RegistrationState.EmailSignUp -> {
|
||||
EmailSignUpView(viewModel = viewModel)
|
||||
}
|
||||
|
||||
RegistrationState.SelfHosted -> {
|
||||
SelfHostedView(viewModel = viewModel)
|
||||
}
|
||||
|
||||
RegistrationState.SocialLogin -> {
|
||||
Text(
|
||||
text = stringResource(id = R.string.welcome_title),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.welcome_subtitle),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
MoreInfoButton()
|
||||
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
|
||||
AuthProviderView(viewModel = viewModel)
|
||||
}
|
||||
|
||||
RegistrationState.PendingUser -> {
|
||||
CreateUserProfileView(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
|
||||
if (viewModel.errorMessage != null) {
|
||||
coroutineScope.launch {
|
||||
val result = snackBarHostState.showSnackbar(
|
||||
viewModel.errorMessage!!,
|
||||
actionLabel = "Dismiss",
|
||||
duration = SnackbarDuration.Indefinite
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed -> viewModel.resetErrorMessage()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
SnackbarHost(hostState = snackBarHostState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthProviderView(viewModel: LoginViewModel) {
|
||||
val isGoogleAuthAvailable: Boolean =
|
||||
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(LocalContext.current) == 0
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (isGoogleAuthAvailable) {
|
||||
GoogleAuthButton(viewModel)
|
||||
}
|
||||
|
||||
AppleAuthButton(viewModel)
|
||||
|
||||
ClickableText(text = AnnotatedString(stringResource(R.string.welcome_screen_action_continue_with_email)),
|
||||
style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignIn() })
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.welcome_screen_action_self_hosting_options)),
|
||||
style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showSelfHostedSettings() },
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MoreInfoButton() {
|
||||
val context = LocalContext.current
|
||||
val intent = remember { Intent(Intent.ACTION_VIEW, Uri.parse("https://omnivore.app/about")) }
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(
|
||||
stringResource(id = R.string.learn_more),
|
||||
),
|
||||
style = MaterialTheme.typography.titleSmall.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = {
|
||||
context.startActivity(intent)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.*
|
||||
@ -18,15 +18,15 @@ import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.save.SaveState
|
||||
import app.omnivore.omnivore.ui.save.SaveViewModel
|
||||
import app.omnivore.omnivore.feature.save.SaveState
|
||||
import app.omnivore.omnivore.feature.save.SaveViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddLinkSheetContent(
|
||||
viewModel: SaveViewModel,
|
||||
@ -84,42 +84,47 @@ fun AddLinkSheetContent(
|
||||
viewModel.saveURL(url)
|
||||
}
|
||||
|
||||
Surface(
|
||||
androidx.compose.material.Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.add_link_sheet_title))
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
),
|
||||
navigationIcon = {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = stringResource(R.string.label_selection_sheet_action_cancel))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
TextButton(onClick = { addLink(textFieldValue.text) }) {
|
||||
Text(stringResource(R.string.add_link_sheet_action_add_link))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 5.dp)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(horizontal = 10.dp)
|
||||
) {
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = stringResource(R.string.add_link_sheet_action_cancel))
|
||||
}
|
||||
|
||||
Text(stringResource(R.string.add_link_sheet_title), fontWeight = FontWeight.ExtraBold)
|
||||
|
||||
TextButton(onClick = { addLink(textFieldValue.text) }) {
|
||||
Text(stringResource(R.string.add_link_sheet_action_add_link))
|
||||
}
|
||||
}
|
||||
|
||||
if (isSaving.value == true) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.width(16.dp),
|
||||
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@ -129,18 +134,29 @@ fun AddLinkSheetContent(
|
||||
value = textFieldValue,
|
||||
placeholder = { Text(stringResource(R.string.add_link_sheet_text_field_placeholder)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||
leadingIcon = { Icon(imageVector = Icons.Default.Link, contentDescription = "linkIcon") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Link,
|
||||
contentDescription = "linkIcon"
|
||||
)
|
||||
},
|
||||
onValueChange = { textFieldValue = it },
|
||||
modifier = Modifier.focusRequester(focusRequester).padding(top = 24.dp).fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.padding(top = 24.dp)
|
||||
.padding(horizontal = 10.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
if (clipboardText != null) {
|
||||
Button(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
modifier = Modifier.padding(top = 10.dp) .padding(horizontal = 10.dp)
|
||||
,
|
||||
onClick = {
|
||||
textFieldValue = TextFieldValue(
|
||||
text = clipboardText,
|
||||
selection = TextRange(clipboardText.length))
|
||||
selection = TextRange(clipboardText.length)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.add_link_sheet_action_paste_from_clipboard))
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
enum class HighlightColorPaletteMode(val backgroundColor: Color) {
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||