mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #3523 from stefanosansone/feat/android-label-chips-m3-update
Android - Update label chips to M3
This commit is contained in:
commit
150444efb4
126 changed files with 3506 additions and 3319 deletions
|
|
@ -3,10 +3,10 @@ plugins {
|
|||
id 'org.jetbrains.kotlin.android'
|
||||
id 'kotlin-kapt'
|
||||
id 'dagger.hilt.android.plugin'
|
||||
id 'com.apollographql.apollo3' version '3.7.2'
|
||||
id("com.apollographql.apollo3") version "3.8.2"
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("app/external/keystore.properties");
|
||||
def keystorePropertiesFile = rootProject.file("app/external/keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
|
@ -19,8 +19,8 @@ android {
|
|||
applicationId "app.omnivore.omnivore"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 188
|
||||
versionName "0.0.188"
|
||||
versionCode 189
|
||||
versionName "0.189.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
|
@ -67,53 +67,60 @@ android {
|
|||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = '1.3.1'
|
||||
kotlinCompilerExtensionVersion = "1.5.8"
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
namespace 'app.omnivore.omnivore'
|
||||
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.8.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'com.google.android.gms:play-services-base:18.1.0'
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.gms:play-services-base:18.3.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||
|
||||
implementation "androidx.navigation:navigation-compose:$nav_version"
|
||||
// Compose
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.01.00')
|
||||
implementation(composeBom)
|
||||
androidTestImplementation(composeBom)
|
||||
|
||||
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"
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.ui:ui:")
|
||||
implementation("androidx.compose.material:material")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
implementation("androidx.activity:activity-compose:1.8.2")
|
||||
implementation("androidx.navigation:navigation-compose:2.7.6")
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
|
||||
// Testing
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
|
||||
// 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")
|
||||
implementation("androidx.compose.runtime:runtime-livedata:1.6.0")
|
||||
|
||||
// Saved state module for ViewModel
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version")
|
||||
|
|
@ -125,45 +132,44 @@ dependencies {
|
|||
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 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
|
||||
|
||||
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"
|
||||
// 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("com.apollographql.apollo3:apollo-runtime:3.8.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.android.gms:play-services-auth:20.7.0")
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:0.34.0")
|
||||
implementation("com.google.accompanist:accompanist-flowlayout:0.32.0")
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'com.pspdfkit:pspdfkit:8.9.1'
|
||||
implementation("io.coil-kt:coil-compose:2.5.0")
|
||||
|
||||
implementation 'com.posthog.android:posthog:2.0.3'
|
||||
implementation 'io.intercom.android:intercom-sdk:15.1.0'
|
||||
implementation("com.google.code.gson:gson:2.10")
|
||||
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("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"
|
||||
implementation("com.github.jeziellago:compose-markdown:0.3.3")
|
||||
implementation("io.github.dokar3:chiptextfield-m3:0.6.5")
|
||||
}
|
||||
|
||||
apollo {
|
||||
packageName.set 'app.omnivore.omnivore.graphql.generated'
|
||||
packageName.set "app.omnivore.omnivore.graphql.generated"
|
||||
}
|
||||
|
||||
task printVersion {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.save.SaveSheetActivity"
|
||||
android:name=".feature.save.SaveSheetActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppCompat.Translucent">
|
||||
<intent-filter>
|
||||
|
|
@ -47,12 +47,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,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()
|
||||
}
|
||||
}
|
||||
|
|
@ -12,15 +12,15 @@ import androidx.compose.ui.Modifier
|
|||
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.LibraryViewModel
|
||||
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.settings.SettingsViewModel
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import com.pspdfkit.PSPDFKit
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
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 app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.database.AppDatabase
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import javax.inject.Inject
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemAndHighlightCrossRef
|
||||
import app.omnivore.omnivore.persistence.entities.saveHighlightChange
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import 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 app.omnivore.omnivore.core.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 com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -53,7 +52,7 @@ suspend fun DataService.createWebHighlight(jsonString: String, colorName: String
|
|||
}
|
||||
|
||||
suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String {
|
||||
val shortId = NanoId.generate(size=14)
|
||||
val shortId = NanoId.generate(size = 14)
|
||||
val createHighlightId = UUID.randomUUID().toString()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
|
|
@ -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.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,28 @@
|
|||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
||||
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) {
|
||||
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,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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,21 +1,24 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.PrimaryKey
|
||||
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 app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.HighlightChange
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.highlightChangeToHighlight
|
||||
import app.omnivore.omnivore.persistence.entities.saveHighlightChange
|
||||
import app.omnivore.omnivore.core.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 com.apollographql.apollo3.api.Optional
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.log
|
||||
|
||||
suspend fun DataService.startSyncChannels() {
|
||||
Log.d("sync", "Starting sync channels")
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package app.omnivore.omnivore.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
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.SavedItemDao
|
||||
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
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun viewerDao(): ViewerDao
|
||||
abstract fun savedItemDao(): SavedItemDao
|
||||
abstract fun highlightDao(): HighlightDao
|
||||
abstract fun highlightChangesDao(): HighlightChangesDao
|
||||
abstract fun savedItemLabelDao(): SavedItemLabelDao
|
||||
abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao
|
||||
abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao
|
||||
abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.persistence
|
||||
package app.omnivore.omnivore.core.database
|
||||
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
|
|
@ -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.model.ServerSyncStatus
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.Dao
|
||||
|
|
@ -9,11 +9,9 @@ import androidx.room.PrimaryKey
|
|||
import androidx.room.Query
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Entity
|
||||
@TypeConverters(StringListTypeConverter::class)
|
||||
|
|
@ -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,268 @@
|
|||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import java.util.*
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface SavedItemDao {
|
||||
@Query("SELECT * FROM savedItem")
|
||||
fun getAll(): List<SavedItem>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
|
||||
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
|
||||
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>
|
||||
): LiveData<List<SavedItemWithLabelsAndHighlights>>
|
||||
|
||||
fun filteredLibraryData(
|
||||
allowedArchiveStates: List<Int>,
|
||||
sortKey: String,
|
||||
requiredLabels: List<String>,
|
||||
excludedLabels: List<String>,
|
||||
allowedContentReaders: List<String>
|
||||
): LiveData<List<SavedItemWithLabelsAndHighlights>> {
|
||||
return _filteredLibraryData(
|
||||
allowedArchiveStates = allowedArchiveStates,
|
||||
sortKey = sortKey,
|
||||
hasRequiredLabels = requiredLabels.size,
|
||||
hasExcludedLabels = excludedLabels.size,
|
||||
requiredLabels = requiredLabels,
|
||||
excludedLabels = excludedLabels,
|
||||
allowedContentReaders = allowedContentReaders
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 "
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
||||
|
||||
@Entity
|
||||
data class SavedItemLabel(
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package app.omnivore.omnivore.core.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)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.graphql.generated.CreateHighlightMutation
|
||||
|
|
@ -10,7 +10,7 @@ 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
|
||||
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.Constants
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.utils.Constants
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -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(
|
||||
|
|
@ -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.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 {
|
||||
|
|
@ -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,8 +1,11 @@
|
|||
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.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||
import app.omnivore.omnivore.core.datastore.OmnivoreDatastore
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -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.*
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -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
|
||||
|
|
@ -27,7 +27,7 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.auth.AuthUtils.autofill
|
||||
import app.omnivore.omnivore.feature.auth.AuthUtils.autofill
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
|
|
@ -29,7 +29,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.auth.AuthUtils.autofill
|
||||
import app.omnivore.omnivore.feature.auth.AuthUtils.autofill
|
||||
|
||||
@Composable
|
||||
fun EmailSignUpView(viewModel: LoginViewModel) {
|
||||
|
|
@ -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")
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.auth
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
|
|
@ -21,7 +21,7 @@ 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.ui.theme.OmnivoreTheme
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -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,14 +18,13 @@ 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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Canvas
|
||||
|
|
@ -8,7 +6,6 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -20,22 +17,15 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircle
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
|
@ -45,7 +35,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -58,86 +47,21 @@ import androidx.compose.ui.platform.LocalViewConfiguration
|
|||
import androidx.compose.ui.platform.ViewConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import com.dokar.chiptextfield.Chip
|
||||
import com.dokar.chiptextfield.ChipTextField
|
||||
import com.dokar.chiptextfield.ChipTextFieldDefaults
|
||||
import com.dokar.chiptextfield.ChipTextFieldState
|
||||
import com.dokar.chiptextfield.m3.ChipTextField
|
||||
import com.dokar.chiptextfield.m3.ChipTextFieldDefaults
|
||||
import com.dokar.chiptextfield.rememberChipTextFieldState
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
|
||||
|
||||
//@Composable
|
||||
//fun LabelsSelectionSheet(viewModel: LibraryViewModel) {
|
||||
// val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false)
|
||||
// val labels: List<SavedItemLabel> by viewModel.savedItemLabelsLiveData.observeAsState(listOf())
|
||||
// val currentSavedItemData = viewModel.currentSavedItemUnderEdit()
|
||||
//
|
||||
// val modalBottomSheetState = rememberModalBottomSheetState(
|
||||
// ModalBottomSheetValue.HalfExpanded,
|
||||
// confirmStateChange = { it != ModalBottomSheetValue.Hidden }
|
||||
// )
|
||||
//
|
||||
// if (isActive) {
|
||||
// ModalBottomSheetLayout(
|
||||
// sheetBackgroundColor = Color.Transparent,
|
||||
// sheetState = modalBottomSheetState,
|
||||
// sheetContent = {
|
||||
// BottomSheetUI {
|
||||
// if (currentSavedItemData != null) {
|
||||
// LabelsSelectionSheetContent(
|
||||
// labels = labels,
|
||||
// initialSelectedLabels = currentSavedItemData.labels,
|
||||
// onCancel = {
|
||||
// viewModel.showLabelsSelectionSheetLiveData.value = false
|
||||
// viewModel.labelsSelectionCurrentItemLiveData.value = null
|
||||
// },
|
||||
// isLibraryMode = false,
|
||||
// onSave = {
|
||||
// if (it != labels) {
|
||||
// viewModel.updateSavedItemLabels(
|
||||
// savedItemID = currentSavedItemData.savedItem.savedItemId,
|
||||
// labels = it
|
||||
// )
|
||||
// }
|
||||
// viewModel.labelsSelectionCurrentItemLiveData.value = null
|
||||
// viewModel.showLabelsSelectionSheetLiveData.value = false
|
||||
// },
|
||||
// onCreateLabel = { newLabelName, labelHexValue ->
|
||||
// viewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
|
||||
// }
|
||||
// )
|
||||
// } else { // Is used in library mode
|
||||
// LabelsSelectionSheetContent(
|
||||
// labels = labels,
|
||||
// initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf(),
|
||||
// onCancel = { viewModel.showLabelsSelectionSheetLiveData.value = false },
|
||||
// isLibraryMode = true,
|
||||
// onSave = {
|
||||
// viewModel.updateAppliedLabels(it)
|
||||
// viewModel.labelsSelectionCurrentItemLiveData.value = null
|
||||
// viewModel.showLabelsSelectionSheetLiveData.value = false
|
||||
// },
|
||||
// onCreateLabel = { newLabelName, labelHexValue ->
|
||||
// viewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ) {}
|
||||
// }
|
||||
//}
|
||||
|
||||
@Composable
|
||||
fun CircleIcon(colorHex: String) {
|
||||
val chipColors = LabelChipColors.fromHex(colorHex)
|
||||
|
|
@ -250,10 +174,7 @@ fun findOrCreateLabel(
|
|||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(
|
||||
ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class,
|
||||
ExperimentalMaterial3Api::class
|
||||
)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun LabelsSelectionSheetContent(
|
||||
isLibraryMode: Boolean,
|
||||
labels: List<SavedItemLabel>,
|
||||
|
|
@ -355,10 +276,10 @@ fun LabelsSelectionSheetContent(
|
|||
focusedBorderColor = Color(0xFF2A2A2A),
|
||||
focusedBackgroundColor = Color(0xFF2A2A2A)
|
||||
),
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
/* colors = androidx.compose.material3.TextFieldDefaults.colors(
|
||||
textColor = MaterialTheme.colorScheme.onBackground,
|
||||
backgroundColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
),*/
|
||||
contentPadding = PaddingValues(10.dp),
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 45.dp)
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.components
|
||||
package app.omnivore.omnivore.feature.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.editinfo
|
||||
package app.omnivore.omnivore.feature.editinfo
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.*
|
||||
|
|
@ -11,7 +11,6 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.editinfo
|
||||
package app.omnivore.omnivore.feature.editinfo
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -6,14 +6,14 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.Constants
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.utils.Constants
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.dataService.DataService
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.graphql.generated.UpdatePageMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdatePageInput
|
||||
import app.omnivore.omnivore.ui.ResourceProvider
|
||||
import app.omnivore.omnivore.feature.ResourceProvider
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
enum class LibraryBottomSheetState {
|
||||
HIDDEN,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
|
|
@ -17,8 +17,8 @@ import androidx.compose.ui.text.intl.Locale
|
|||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.ui.components.LabelChipColors
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.feature.components.LabelChipColors
|
||||
|
||||
@Composable
|
||||
fun LibraryFilterBar(viewModel: LibraryViewModel) {
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LibraryNavigationBar(
|
||||
savedItemViewModel: SavedItemViewModel,
|
||||
onSearchClicked: () -> Unit,
|
||||
onAddLinkClicked: () -> Unit,
|
||||
onSettingsIconClick: () -> Unit
|
||||
) {
|
||||
val actionsMenuItem: SavedItemWithLabelsAndHighlights? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(
|
||||
null
|
||||
)
|
||||
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
if (actionsMenuItem == null)
|
||||
stringResource(R.string.library_nav_bar_title) else
|
||||
stringResource(R.string.library_nav_bar_title_alt)
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = if (actionsMenuItem == null) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
navigationIcon = {
|
||||
if (actionsMenuItem != null) {
|
||||
IconButton(onClick = {
|
||||
savedItemViewModel.actionsMenuItemLiveData.postValue(null)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
actionsMenuItem?.let {
|
||||
IconButton(onClick = {
|
||||
savedItemViewModel.handleSavedItemAction(
|
||||
it.savedItem.savedItemId,
|
||||
if (it.savedItem.isArchived) SavedItemAction.Unarchive else SavedItemAction.Archive
|
||||
)
|
||||
}) {
|
||||
if (it.savedItem.isArchived) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.unarchive),
|
||||
contentDescription = null
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.archive_outline),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
savedItemViewModel.handleSavedItemAction(
|
||||
it.savedItem.savedItemId,
|
||||
SavedItemAction.EditInfo
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Outlined.Info,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
savedItemViewModel.handleSavedItemAction(
|
||||
it.savedItem.savedItemId,
|
||||
SavedItemAction.EditLabels
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.tag),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
savedItemViewModel.handleSavedItemAction(
|
||||
it.savedItem.savedItemId,
|
||||
SavedItemAction.Delete
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
/* IconButton(onClick = { isMenuExpanded = true } ) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null
|
||||
)
|
||||
if (isMenuExpanded) {
|
||||
SavedItemLibraryContextMenu(
|
||||
savedItemViewModel = savedItemViewModel,
|
||||
savedItem = it.savedItem,
|
||||
isExpanded = true,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
)
|
||||
}
|
||||
}*/
|
||||
} ?: run {
|
||||
IconButton(onClick = onSearchClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onAddLinkClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onSettingsIconClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchField(
|
||||
searchText: String,
|
||||
onSearch: () -> Unit,
|
||||
onSearchTextChanged: (String) -> Unit,
|
||||
navController: NavHostController,
|
||||
) {
|
||||
var showClearButton by remember { mutableStateOf(false) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { focusState ->
|
||||
showClearButton = (focusState.isFocused)
|
||||
}
|
||||
.focusRequester(focusRequester),
|
||||
value = searchText,
|
||||
onValueChange = onSearchTextChanged,
|
||||
placeholder = {
|
||||
Text(text = stringResource(R.string.library_nav_bar_field_placeholder_search))
|
||||
},
|
||||
leadingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onSearchTextChanged("")
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
AnimatedVisibility(
|
||||
visible = showClearButton,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
IconButton(onClick = { onSearchTextChanged("") }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = {
|
||||
onSearch()
|
||||
}),
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
|
|
@ -53,19 +53,19 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.ui.components.AddLinkSheetContent
|
||||
import app.omnivore.omnivore.ui.components.LabelsSelectionSheetContent
|
||||
import app.omnivore.omnivore.ui.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.ui.editinfo.EditInfoSheetContent
|
||||
import app.omnivore.omnivore.ui.editinfo.EditInfoViewModel
|
||||
import app.omnivore.omnivore.ui.reader.PDFReaderActivity
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity
|
||||
import app.omnivore.omnivore.ui.save.SaveState
|
||||
import app.omnivore.omnivore.ui.save.SaveViewModel
|
||||
import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.feature.components.AddLinkSheetContent
|
||||
import app.omnivore.omnivore.feature.components.LabelsSelectionSheetContent
|
||||
import app.omnivore.omnivore.feature.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.feature.editinfo.EditInfoSheetContent
|
||||
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
|
||||
import app.omnivore.omnivore.feature.reader.PDFReaderActivity
|
||||
import app.omnivore.omnivore.feature.reader.WebReaderLoadingContainerActivity
|
||||
import app.omnivore.omnivore.feature.save.SaveState
|
||||
import app.omnivore.omnivore.feature.save.SaveViewModel
|
||||
import app.omnivore.omnivore.feature.savedItemViews.SavedItemCard
|
||||
import app.omnivore.omnivore.navigation.Routes
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -1,20 +1,43 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import app.omnivore.omnivore.*
|
||||
import app.omnivore.omnivore.dataService.*
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.data.archiveSavedItem
|
||||
import app.omnivore.omnivore.core.data.deleteSavedItem
|
||||
import app.omnivore.omnivore.core.data.fetchSavedItemContent
|
||||
import app.omnivore.omnivore.core.data.isSavedItemContentStoredInDB
|
||||
import app.omnivore.omnivore.core.data.librarySearch
|
||||
import app.omnivore.omnivore.core.data.sync
|
||||
import app.omnivore.omnivore.core.data.syncLabels
|
||||
import app.omnivore.omnivore.core.data.syncOfflineItemsWithServerIfNeeded
|
||||
import app.omnivore.omnivore.core.data.unarchiveSavedItem
|
||||
import app.omnivore.omnivore.core.data.updateWebReadingProgress
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
import app.omnivore.omnivore.ui.ResourceProvider
|
||||
import app.omnivore.omnivore.ui.setSavedItemLabels
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.createNewLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.feature.ResourceProvider
|
||||
import app.omnivore.omnivore.feature.setSavedItemLabels
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -25,8 +48,8 @@ class LibraryViewModel @Inject constructor(
|
|||
private val datastoreRepo: DatastoreRepository,
|
||||
private val resourceProvider: ResourceProvider
|
||||
) : ViewModel(), SavedItemViewModel {
|
||||
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
|
||||
|
||||
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
|
||||
private var cursor: String? = null
|
||||
private var librarySearchCursor: String? = null
|
||||
|
||||
|
|
@ -175,12 +198,6 @@ class LibraryViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// fun sortKey(appliedSortKey: String) {
|
||||
// when(appliedSortKey) {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun handleFilterChanges() {
|
||||
librarySearchCursor = null
|
||||
|
||||
|
|
@ -307,6 +324,34 @@ class LibraryViewModel @Inject constructor(
|
|||
currentItemLiveData.value = itemID
|
||||
bottomSheetState.value = LibraryBottomSheetState.EDIT
|
||||
}
|
||||
|
||||
SavedItemAction.MarkRead -> {
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebReadingProgress(
|
||||
jsonString = Gson().toJson(
|
||||
mapOf(
|
||||
"id" to itemID,
|
||||
"readingProgressPercent" to 100.0,
|
||||
"readingProgressAnchorIndex" to 0
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemAction.MarkUnread -> {
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebReadingProgress(
|
||||
jsonString = Gson().toJson(
|
||||
mapOf(
|
||||
"id" to itemID,
|
||||
"readingProgressPercent" to 0,
|
||||
"readingProgressAnchorIndex" to 0
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
actionsMenuItemLiveData.postValue(null)
|
||||
}
|
||||
|
|
@ -405,4 +450,6 @@ enum class SavedItemAction {
|
|||
Unarchive,
|
||||
EditLabels,
|
||||
EditInfo,
|
||||
MarkRead,
|
||||
MarkUnread
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
|
||||
@Composable
|
||||
fun SavedItemLibraryContextMenu(
|
||||
savedItemViewModel: SavedItemViewModel,
|
||||
savedItem: SavedItem,
|
||||
isExpanded: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val menuOptions = listOf(
|
||||
if (savedItemViewModel.actionsMenuItemLiveData.value?.savedItem?.readingProgress == 100.0) {
|
||||
MenuItemOption(R.string.saved_item_context_menu_action_mark_unread, SavedItemAction.MarkUnread)
|
||||
} else {
|
||||
MenuItemOption(R.string.saved_item_context_menu_action_mark_read, SavedItemAction.MarkRead)
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
menuOptions.forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text( text = stringResource(option.textResourceId), fontWeight = FontWeight.Normal) },
|
||||
onClick = {
|
||||
savedItemViewModel.handleSavedItemAction(savedItem.savedItemId, option.action)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MenuItemOption(
|
||||
val textResourceId: Int,
|
||||
val action: SavedItemAction,
|
||||
val customAction: (() -> Unit)? = null
|
||||
)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
enum class SavedItemSortFilter(
|
||||
val displayText: String,
|
||||
val rawValue: String,
|
||||
val queryString: String
|
||||
) {
|
||||
NEWEST("Newest", rawValue = "newest", "sort:saved"),
|
||||
OLDEST("Oldest", rawValue = "oldest", "sort:saved-ASC"),
|
||||
RECENTLY_READ("Recently Read", rawValue = "recentlyRead", "sort:read"),
|
||||
RECENTLY_PUBLISHED("Recently Published", rawValue = "recentlyPublished", "sort:published"),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SavedItemSortFilterContextMenu(
|
||||
isExpanded: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
actionHandler: (SavedItemSortFilter) -> Unit
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
SavedItemSortFilter.values().forEach {
|
||||
DropdownMenuItem(
|
||||
text = { Text(it.displayText) },
|
||||
onClick = {
|
||||
actionHandler(it)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
|
||||
interface SavedItemViewModel {
|
||||
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.database.entities.TypeaheadCardData
|
||||
import app.omnivore.omnivore.feature.reader.PDFReaderActivity
|
||||
import app.omnivore.omnivore.feature.reader.WebReaderLoadingContainerActivity
|
||||
import app.omnivore.omnivore.feature.savedItemViews.SavedItemCard
|
||||
import app.omnivore.omnivore.feature.savedItemViews.TypeaheadSearchCard
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchView(
|
||||
viewModel: SearchViewModel,
|
||||
navController: NavHostController
|
||||
) {
|
||||
val isRefreshing: Boolean by viewModel.isRefreshing.observeAsState(false)
|
||||
val typeaheadMode: Boolean by viewModel.typeaheadMode.observeAsState(true)
|
||||
val searchText: String by viewModel.searchTextLiveData.observeAsState("")
|
||||
val actionsMenuItem: SavedItemWithLabelsAndHighlights? by viewModel.actionsMenuItemLiveData.observeAsState(
|
||||
null
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
navigationIcon = {
|
||||
if (actionsMenuItem != null) {
|
||||
IconButton(onClick = {
|
||||
viewModel.actionsMenuItemLiveData.postValue(null)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (actionsMenuItem != null) {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.archive_outline),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.tag),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
SearchField(
|
||||
searchText,
|
||||
onSearch = {
|
||||
viewModel.performSearch()
|
||||
},
|
||||
onSearchTextChanged = { viewModel.updateSearchText(it) },
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (isRefreshing) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(45.dp)
|
||||
.width(45.dp),
|
||||
strokeWidth = 5.dp,
|
||||
color = colorResource(R.color.green_55B938)
|
||||
)
|
||||
}
|
||||
} else if (typeaheadMode) {
|
||||
TypeaheadSearchViewContent(
|
||||
viewModel,
|
||||
modifier = Modifier
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
)
|
||||
} else {
|
||||
SearchViewContent(
|
||||
viewModel,
|
||||
modifier = Modifier
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TypeaheadSearchViewContent(viewModel: SearchViewModel, modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val searchedCardsData: List<TypeaheadCardData> by viewModel.searchItemsLiveData.observeAsState(
|
||||
listOf()
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
items(searchedCardsData) { cardData ->
|
||||
TypeaheadSearchCard(
|
||||
cardData = cardData,
|
||||
onClickHandler = {
|
||||
// val activityClass = if (cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java
|
||||
val activityClass = WebReaderLoadingContainerActivity::class.java
|
||||
val intent = Intent(context, activityClass)
|
||||
intent.putExtra("SAVED_ITEM_SLUG", cardData.slug)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
actionHandler = { viewModel.handleSavedItemAction(cardData.savedItemId, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchViewContent(viewModel: SearchViewModel, modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val cardsData: List<SavedItemWithLabelsAndHighlights> by viewModel.itemsLiveData.observeAsState(
|
||||
listOf()
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
items(cardsData) { cardDataWithLabels ->
|
||||
SavedItemCard(
|
||||
selected = false,
|
||||
savedItemViewModel = viewModel,
|
||||
savedItem = cardDataWithLabels,
|
||||
onClickHandler = {
|
||||
val activityClass =
|
||||
if (cardDataWithLabels.savedItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java
|
||||
val intent = Intent(context, activityClass)
|
||||
intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.savedItem.slug)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
actionHandler = {
|
||||
viewModel.handleSavedItemAction(
|
||||
cardDataWithLabels.savedItem.savedItemId,
|
||||
it
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,27 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.*
|
||||
import app.omnivore.omnivore.dataService.*
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.data.archiveSavedItem
|
||||
import app.omnivore.omnivore.core.data.deleteSavedItem
|
||||
import app.omnivore.omnivore.core.data.isSavedItemContentStoredInDB
|
||||
import app.omnivore.omnivore.core.data.librarySearch
|
||||
import app.omnivore.omnivore.core.data.unarchiveSavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.database.entities.TypeaheadCardData
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.typeaheadSearch
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -18,7 +29,7 @@ class SearchViewModel @Inject constructor(
|
|||
private val networker: Networker,
|
||||
private val dataService: DataService,
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
): ViewModel(), SavedItemViewModel {
|
||||
) : ViewModel(), SavedItemViewModel {
|
||||
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
|
||||
|
||||
private var cursor: String? = null
|
||||
|
|
@ -66,7 +77,10 @@ class SearchViewModel @Inject constructor(
|
|||
private fun loadUsingSearchAPI() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = dataService.librarySearch(cursor = librarySearchCursor, query = searchQueryString())
|
||||
val result = dataService.librarySearch(
|
||||
cursor = librarySearchCursor,
|
||||
query = searchQueryString()
|
||||
)
|
||||
result.cursor?.let {
|
||||
librarySearchCursor = it
|
||||
}
|
||||
|
|
@ -81,29 +95,29 @@ class SearchViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
val newItems = result.savedItems
|
||||
/*
|
||||
.map {
|
||||
SavedItemCardDataWithLabels(
|
||||
cardData = SavedItemCardData(
|
||||
savedItemId = it.savedItem.savedItemId,
|
||||
slug = it.savedItem.slug,
|
||||
publisherURLString = it.savedItem.publisherURLString,
|
||||
title = it.savedItem.title,
|
||||
author = it.savedItem.author,
|
||||
imageURLString = it.savedItem.imageURLString,
|
||||
isArchived = it.savedItem.isArchived,
|
||||
pageURLString = it.savedItem.pageURLString,
|
||||
contentReader = it.savedItem.contentReader,
|
||||
savedAt = it.savedItem.savedAt,
|
||||
readingProgress = it.savedItem.readingProgress,
|
||||
wordsCount = it.savedItem.wordsCount
|
||||
),
|
||||
labels = listOf()
|
||||
)
|
||||
}
|
||||
*/
|
||||
/*
|
||||
.map {
|
||||
SavedItemCardDataWithLabels(
|
||||
cardData = SavedItemCardData(
|
||||
savedItemId = it.savedItem.savedItemId,
|
||||
slug = it.savedItem.slug,
|
||||
publisherURLString = it.savedItem.publisherURLString,
|
||||
title = it.savedItem.title,
|
||||
author = it.savedItem.author,
|
||||
imageURLString = it.savedItem.imageURLString,
|
||||
isArchived = it.savedItem.isArchived,
|
||||
pageURLString = it.savedItem.pageURLString,
|
||||
contentReader = it.savedItem.contentReader,
|
||||
savedAt = it.savedItem.savedAt,
|
||||
readingProgress = it.savedItem.readingProgress,
|
||||
wordsCount = it.savedItem.wordsCount
|
||||
),
|
||||
labels = listOf()
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
itemsLiveData.value?.let{
|
||||
itemsLiveData.value?.let {
|
||||
itemsLiveData.postValue(newItems + it)
|
||||
} ?: run {
|
||||
itemsLiveData.postValue(newItems)
|
||||
|
|
@ -146,22 +160,29 @@ class SearchViewModel @Inject constructor(
|
|||
dataService.deleteSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemAction.Archive -> {
|
||||
viewModelScope.launch {
|
||||
dataService.archiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemAction.Unarchive -> {
|
||||
viewModelScope.launch {
|
||||
dataService.unarchiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemAction.EditLabels -> {
|
||||
// TODO
|
||||
}
|
||||
|
||||
SavedItemAction.EditInfo -> {
|
||||
// TODO
|
||||
}
|
||||
|
||||
SavedItemAction.MarkRead -> TODO()
|
||||
SavedItemAction.MarkUnread -> TODO()
|
||||
}
|
||||
actionsMenuItemLiveData.postValue(null)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.notebook
|
||||
package app.omnivore.omnivore.feature.notebook
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
|
|
@ -6,7 +6,6 @@ import android.content.Context
|
|||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
|
|
@ -36,11 +35,11 @@ import androidx.compose.ui.text.TextStyle
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
import kotlinx.coroutines.launch
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package app.omnivore.omnivore.ui.notebook
|
||||
package app.omnivore.omnivore.feature.notebook
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import app.omnivore.omnivore.dataService.DataService
|
||||
import app.omnivore.omnivore.dataService.createNoteHighlight
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.data.createNoteHighlight
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import app.omnivore.omnivore.networking.updateHighlight
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.updateHighlight
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
|
|
@ -9,8 +9,8 @@ import android.view.WindowManager
|
|||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import app.omnivore.omnivore.ui.notebook.EditNoteModal
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
import app.omnivore.omnivore.feature.notebook.EditNoteModal
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
|
|
@ -19,7 +19,7 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import com.pspdfkit.annotations.Annotation
|
||||
import com.pspdfkit.annotations.HighlightAnnotation
|
||||
import com.pspdfkit.configuration.PdfConfiguration
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
|
@ -6,13 +6,20 @@ import android.util.Log
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.dataService.DataService
|
||||
import app.omnivore.omnivore.dataService.NanoId
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
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.mergeHighlights
|
||||
import app.omnivore.omnivore.core.network.savedItem
|
||||
import app.omnivore.omnivore.core.network.updateHighlight
|
||||
import app.omnivore.omnivore.core.network.updateReadingProgress
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.data.NanoId
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
import com.pspdfkit.annotations.Annotation
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
|
|
@ -24,7 +24,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
|
||||
@Composable
|
||||
fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
||||
|
|
@ -0,0 +1,361 @@
|
|||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.graphics.drawable.shapes.RoundRectShape
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.View.OnScrollChangeListener
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import app.omnivore.omnivore.R
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun WebReader(
|
||||
styledContent: String, webReaderViewModel: WebReaderViewModel, currentTheme: Themes?
|
||||
) {
|
||||
val javascriptActionLoopUUID: UUID by webReaderViewModel.javascriptActionLoopUUIDLiveData.observeAsState(
|
||||
UUID.randomUUID()
|
||||
)
|
||||
val isDarkMode = isSystemInDarkTheme()
|
||||
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
|
||||
Box {
|
||||
AndroidView(factory = {
|
||||
OmnivoreWebView(it).apply {
|
||||
viewModel = webReaderViewModel
|
||||
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
settings.javaScriptEnabled = true
|
||||
settings.allowContentAccess = true
|
||||
settings.allowFileAccess = true
|
||||
settings.domStorageEnabled = true
|
||||
|
||||
alpha = 1.0f
|
||||
viewModel?.showNavBar()
|
||||
currentTheme?.let { theme ->
|
||||
val bg = when (theme) {
|
||||
Themes.SYSTEM -> {
|
||||
if (isDarkMode) {
|
||||
Color.BLACK
|
||||
} else {
|
||||
Color.WHITE
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
theme.backgroundColor
|
||||
}
|
||||
}
|
||||
setBackgroundColor(bg.toInt())
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val scrollbarColor = when (theme) {
|
||||
Themes.SYSTEM -> {
|
||||
if (isDarkMode) {
|
||||
Color.WHITE
|
||||
} else {
|
||||
Color.BLACK
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
theme.scrollbarColor
|
||||
}
|
||||
}
|
||||
|
||||
val rad = 8f
|
||||
val shape = ShapeDrawable(
|
||||
RoundRectShape(
|
||||
floatArrayOf(
|
||||
rad, rad, rad, rad, rad, rad, rad, rad
|
||||
), null, null
|
||||
)
|
||||
)
|
||||
shape.paint.color = scrollbarColor.toInt()
|
||||
verticalScrollbarThumbDrawable = shape
|
||||
}
|
||||
}
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?, request: WebResourceRequest?
|
||||
): Boolean {
|
||||
var handled: Boolean? = null
|
||||
request?.let {
|
||||
if (request.isForMainFrame && request.hasGesture() && viewModel != null) {
|
||||
viewModel?.showOpenLinkSheet(context, request.url)
|
||||
handled = true
|
||||
}
|
||||
}
|
||||
|
||||
return handled ?: super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
}
|
||||
|
||||
val javascriptInterface = AndroidWebKitMessenger { actionID, json ->
|
||||
webReaderViewModel.hasTappedExistingHighlight = false
|
||||
|
||||
when (actionID) {
|
||||
"userTap" -> {
|
||||
val tapCoordinates = Gson().fromJson(json, TapCoordinates::class.java)
|
||||
Log.d("wvt", "received tap action: $tapCoordinates")
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
webReaderViewModel.lastTapCoordinates = tapCoordinates
|
||||
actionMode?.finish()
|
||||
actionMode = null
|
||||
}
|
||||
}
|
||||
|
||||
"existingHighlightTap" -> {
|
||||
val tapCoordinates = Gson().fromJson(json, TapCoordinates::class.java)
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
webReaderViewModel.hasTappedExistingHighlight = true
|
||||
webReaderViewModel.lastTapCoordinates = tapCoordinates
|
||||
startActionMode(null, ActionMode.TYPE_FLOATING)
|
||||
}
|
||||
}
|
||||
|
||||
"writeToClipboard" -> {
|
||||
val quote = Gson().fromJson(json, HighlightQuote::class.java).quote
|
||||
quote.let { unwrappedQuote ->
|
||||
val clipboard =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(unwrappedQuote, unwrappedQuote)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
webReaderViewModel.handleIncomingWebMessage(actionID, json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addJavascriptInterface(javascriptInterface, "AndroidWebKitMessenger")
|
||||
|
||||
loadDataWithBaseURL(
|
||||
"file:///android_asset/",
|
||||
styledContent,
|
||||
"text/html; charset=utf-8",
|
||||
"utf-8",
|
||||
null
|
||||
)
|
||||
requestFocus()
|
||||
setOnKeyListener { _, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
scrollVertically(OmnivoreWebView.Direction.UP)
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
scrollVertically(OmnivoreWebView.Direction.DOWN)
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
}
|
||||
}
|
||||
// default value
|
||||
false
|
||||
}
|
||||
}
|
||||
}, update = {
|
||||
if (javascriptActionLoopUUID != webReaderViewModel.lastJavascriptActionLoopUUID) {
|
||||
for (script in webReaderViewModel.javascriptDispatchQueue) {
|
||||
Log.d("js", "executing script: $script")
|
||||
it.evaluateJavascript(script, null)
|
||||
}
|
||||
webReaderViewModel.resetJavascriptDispatchQueue()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class OmnivoreWebView(context: Context) : WebView(context), OnScrollChangeListener {
|
||||
var viewModel: WebReaderViewModel? = null
|
||||
var actionMode: ActionMode? = null
|
||||
val density = resources.displayMetrics.density
|
||||
|
||||
init {
|
||||
setOnScrollChangeListener(this)
|
||||
}
|
||||
|
||||
enum class Direction(val value: Int) {
|
||||
UP(-1), DOWN(1)
|
||||
}
|
||||
|
||||
fun scrollVertically(direction: Direction, heightFactor: Int = 10) {
|
||||
if (canScrollVertically(direction.value)) {
|
||||
val scrollByValue = height.div(heightFactor)
|
||||
scrollBy(0, direction.value.times(scrollByValue))
|
||||
}
|
||||
}
|
||||
|
||||
private val actionModeCallback = object : ActionMode.Callback2() {
|
||||
// Called when the action mode is created; startActionMode() was called
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
actionMode = mode
|
||||
if (viewModel?.hasTappedExistingHighlight == true) {
|
||||
Log.d("wv", "inflating existing highlight menu")
|
||||
mode.menuInflater.inflate(R.menu.highlight_selection_menu, menu)
|
||||
} else {
|
||||
viewModel?.showHighlightColorPalette()
|
||||
mode.menuInflater.inflate(R.menu.text_selection_menu, menu)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Called each time the action mode is shown. Always called after onCreateActionMode, but
|
||||
// may be called multiple times if the mode is invalidated.
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
return false // Return false if nothing is done
|
||||
}
|
||||
|
||||
// Called when the user selects a contextual menu item
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.annotateHighlight, R.id.annotate -> {
|
||||
val script = "var event = new Event('annotate');document.dispatchEvent(event);"
|
||||
evaluateJavascript(script) {
|
||||
mode.finish()
|
||||
actionMode = null
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.highlight -> {
|
||||
val script = "var event = new Event('highlight');document.dispatchEvent(event);"
|
||||
evaluateJavascript(script) {
|
||||
clearFocus()
|
||||
mode.finish()
|
||||
actionMode = null
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copyHighlight -> {
|
||||
val script =
|
||||
"var event = new Event('copyHighlight');document.dispatchEvent(event);"
|
||||
evaluateJavascript(script) {
|
||||
clearFocus()
|
||||
mode.finish()
|
||||
actionMode = null
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copyTextSelection -> {
|
||||
val script =
|
||||
"var event = new Event('copyTextSelection');document.dispatchEvent(event);"
|
||||
evaluateJavascript(script) {
|
||||
clearFocus()
|
||||
mode.finish()
|
||||
actionMode = null
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.removeHighlight -> {
|
||||
val script = "var event = new Event('remove');document.dispatchEvent(event);"
|
||||
evaluateJavascript(script) {
|
||||
clearFocus()
|
||||
mode.finish()
|
||||
actionMode = null
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.d("Loggo", "${item.itemId} selected")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called when the user exits the action mode
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
Log.d("wv", "destroying menu: $mode")
|
||||
viewModel?.hasTappedExistingHighlight = false
|
||||
viewModel?.hideHighlightColorPalette()
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
override fun onGetContentRect(mode: ActionMode?, view: View?, outRect: Rect?) {
|
||||
Log.d("wv", "outRect: $outRect, View: $view")
|
||||
if (viewModel?.lastTapCoordinates != null) {
|
||||
val xValue = (viewModel!!.lastTapCoordinates!!.tapX * density).toInt()
|
||||
val yValue = (viewModel!!.lastTapCoordinates!!.tapY * density).toInt()
|
||||
val rect = Rect(xValue, yValue, xValue, yValue)
|
||||
|
||||
Log.d(
|
||||
"wvt",
|
||||
"setting rect based on last tapped rect: ${viewModel?.lastTapCoordinates.toString()}"
|
||||
)
|
||||
Log.d("wvt", "rect: $rect")
|
||||
|
||||
outRect?.set(rect)
|
||||
} else {
|
||||
outRect?.set(left, top, right, bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun startActionMode(callback: ActionMode.Callback?): ActionMode {
|
||||
Log.d("wv", "startActionMode:callback called")
|
||||
return super.startActionMode(actionModeCallback)
|
||||
}
|
||||
|
||||
override fun startActionModeForChild(
|
||||
originalView: View?, callback: ActionMode.Callback?
|
||||
): ActionMode {
|
||||
Log.d("wv", "startActionMode:originalView:callback called")
|
||||
return super.startActionModeForChild(originalView, actionModeCallback)
|
||||
}
|
||||
|
||||
override fun startActionMode(callback: ActionMode.Callback?, type: Int): ActionMode {
|
||||
Log.d("wv", "startActionMode:type called")
|
||||
return super.startActionMode(actionModeCallback, type)
|
||||
}
|
||||
|
||||
override fun onScrollChange(view: View?, x: Int, y: Int, oldX: Int, oldY: Int) {
|
||||
viewModel?.onScrollChange((oldY - y).toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidWebKitMessenger(val messageHandler: (String, String) -> Unit) {
|
||||
@JavascriptInterface
|
||||
fun handleIdentifiableMessage(actionID: String, jsonString: String) {
|
||||
messageHandler(actionID, jsonString)
|
||||
}
|
||||
}
|
||||
|
||||
data class TapCoordinates(
|
||||
val tapX: Double, val tapY: Double
|
||||
)
|
||||
|
||||
data class HighlightQuote(val quote: String?)
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import com.google.gson.Gson
|
||||
|
||||
enum class WebFont(val displayText: String, val rawValue: String) {
|
||||
|
|
@ -0,0 +1,528 @@
|
|||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetLayout
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import app.omnivore.omnivore.MainActivity
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.feature.components.LabelsSelectionSheetContent
|
||||
import app.omnivore.omnivore.feature.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.feature.editinfo.EditInfoSheetContent
|
||||
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
|
||||
import app.omnivore.omnivore.feature.notebook.EditNoteModal
|
||||
import app.omnivore.omnivore.feature.notebook.NotebookView
|
||||
import app.omnivore.omnivore.feature.notebook.NotebookViewModel
|
||||
import app.omnivore.omnivore.feature.savedItemViews.SavedItemContextMenu
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WebReaderLoadingContainerActivity : ComponentActivity() {
|
||||
val viewModel: WebReaderViewModel by viewModels()
|
||||
private val notebookViewModel: NotebookViewModel by viewModels()
|
||||
private val labelsViewModel: LabelsViewModel by viewModels()
|
||||
private val editInfoViewModel: EditInfoViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val requestID = intent.getStringExtra("SAVED_ITEM_REQUEST_ID")
|
||||
val slug = intent.getStringExtra("SAVED_ITEM_SLUG")
|
||||
|
||||
viewModel.loadItem(slug = slug, requestID = requestID)
|
||||
|
||||
setContent {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val useDarkIcons = !isSystemInDarkTheme()
|
||||
|
||||
DisposableEffect(systemUiController, useDarkIcons) {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Black,
|
||||
darkIcons = false
|
||||
)
|
||||
|
||||
onDispose {}
|
||||
}
|
||||
|
||||
OmnivoreTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = Color.Black)
|
||||
) {
|
||||
if (viewModel.hasFetchError.value == true) {
|
||||
Text(stringResource(R.string.web_reader_loading_container_error_msg))
|
||||
} else {
|
||||
WebReaderLoadingContainer(
|
||||
onLibraryIconTap = if (requestID != null) {
|
||||
{ startMainActivity() }
|
||||
} else null,
|
||||
webReaderViewModel = viewModel,
|
||||
notebookViewModel = notebookViewModel,
|
||||
labelsViewModel = labelsViewModel,
|
||||
editInfoViewModel = 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
|
||||
}
|
||||
}
|
||||
|
||||
private fun startMainActivity() {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
this.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
enum class BottomSheetState {
|
||||
NONE,
|
||||
PREFERENCES,
|
||||
NOTEBOOK,
|
||||
EDITNOTE,
|
||||
HIGHLIGHTNOTE,
|
||||
LABELS,
|
||||
LINK,
|
||||
EDIT_INFO,
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun WebReaderLoadingContainer(
|
||||
onLibraryIconTap: (() -> Unit)? = null,
|
||||
webReaderViewModel: WebReaderViewModel,
|
||||
notebookViewModel: NotebookViewModel,
|
||||
labelsViewModel: LabelsViewModel,
|
||||
editInfoViewModel: EditInfoViewModel
|
||||
) {
|
||||
val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState()
|
||||
val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value }
|
||||
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
val bottomSheetState: BottomSheetState? by webReaderViewModel.bottomSheetStateLiveData.observeAsState(
|
||||
BottomSheetState.NONE
|
||||
)
|
||||
|
||||
val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(
|
||||
null
|
||||
)
|
||||
val shouldPopView: Boolean by webReaderViewModel.shouldPopViewLiveData.observeAsState(false)
|
||||
|
||||
val labels: List<SavedItemLabel> by webReaderViewModel.savedItemLabelsLiveData.observeAsState(
|
||||
listOf()
|
||||
)
|
||||
|
||||
val maxToolbarHeight = 48.dp
|
||||
webReaderViewModel.maxToolbarHeightPx =
|
||||
with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val styledContent = webReaderParams?.let {
|
||||
val webReaderContent = WebReaderContent(
|
||||
preferences = webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()),
|
||||
item = it.item,
|
||||
articleContent = it.articleContent,
|
||||
)
|
||||
webReaderContent.styledContent()
|
||||
}
|
||||
|
||||
|
||||
val modalBottomSheetState = rememberModalBottomSheetState(
|
||||
initialValue = ModalBottomSheetValue.Hidden,
|
||||
skipHalfExpanded = bottomSheetState == BottomSheetState.EDITNOTE || bottomSheetState == BottomSheetState.HIGHLIGHTNOTE,
|
||||
confirmValueChange = {
|
||||
if (it == ModalBottomSheetValue.Hidden) {
|
||||
webReaderViewModel.resetBottomSheet()
|
||||
}
|
||||
true
|
||||
}
|
||||
)
|
||||
|
||||
val showMenu = {
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.show()
|
||||
}
|
||||
}
|
||||
|
||||
when (bottomSheetState) {
|
||||
BottomSheetState.PREFERENCES -> {
|
||||
coroutineScope.launch {
|
||||
if (!modalBottomSheetState.isVisible) {
|
||||
modalBottomSheetState.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetState.NOTEBOOK, BottomSheetState.EDITNOTE,
|
||||
BottomSheetState.HIGHLIGHTNOTE, BottomSheetState.LABELS, BottomSheetState.EDIT_INFO,
|
||||
BottomSheetState.LINK,
|
||||
-> {
|
||||
showMenu()
|
||||
}
|
||||
|
||||
else -> {
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding(),
|
||||
sheetBackgroundColor = Color.Transparent,
|
||||
sheetState = modalBottomSheetState,
|
||||
sheetContent = {
|
||||
when (bottomSheetState) {
|
||||
BottomSheetState.PREFERENCES -> {
|
||||
BottomSheetUI {
|
||||
ReaderPreferencesView(webReaderViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetState.NOTEBOOK -> {
|
||||
webReaderParams?.let { params ->
|
||||
BottomSheetUI {
|
||||
NotebookView(
|
||||
savedItemId = params.item.savedItemId,
|
||||
viewModel = notebookViewModel,
|
||||
onEditNote = {
|
||||
notebookViewModel.highlightUnderEdit = it
|
||||
webReaderViewModel.setBottomSheet(BottomSheetState.EDITNOTE)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetState.EDITNOTE -> {
|
||||
webReaderParams?.let { params ->
|
||||
EditNoteModal(
|
||||
initialValue = notebookViewModel.highlightUnderEdit?.annotation,
|
||||
onDismiss = { save, note ->
|
||||
coroutineScope.launch {
|
||||
if (save) {
|
||||
notebookViewModel.highlightUnderEdit?.let { highlight ->
|
||||
notebookViewModel.updateHighlightNote(
|
||||
highlight.highlightId,
|
||||
note
|
||||
)
|
||||
} ?: run {
|
||||
if (note != null) {
|
||||
notebookViewModel.addArticleNote(
|
||||
savedItemId = params.item.savedItemId,
|
||||
note = note
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
notebookViewModel.highlightUnderEdit = null
|
||||
}
|
||||
webReaderViewModel.setBottomSheet(BottomSheetState.NOTEBOOK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetState.HIGHLIGHTNOTE -> {
|
||||
EditNoteModal(
|
||||
initialValue = webReaderViewModel.annotation,
|
||||
onDismiss = { save, note ->
|
||||
coroutineScope.launch {
|
||||
if (save) {
|
||||
webReaderViewModel.saveAnnotation(note ?: "")
|
||||
} else {
|
||||
webReaderViewModel.cancelAnnotation()
|
||||
}
|
||||
webReaderViewModel.annotation = null
|
||||
}
|
||||
webReaderViewModel.resetBottomSheet()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
BottomSheetState.LABELS -> {
|
||||
BottomSheetUI {
|
||||
LabelsSelectionSheetContent(
|
||||
labels = labels,
|
||||
labelsViewModel = labelsViewModel,
|
||||
initialSelectedLabels = webReaderParams?.labels ?: listOf(),
|
||||
onCancel = {
|
||||
coroutineScope.launch {
|
||||
webReaderViewModel.resetBottomSheet()
|
||||
}
|
||||
},
|
||||
isLibraryMode = false,
|
||||
onSave = {
|
||||
if (it != labels) {
|
||||
webReaderViewModel.updateSavedItemLabels(
|
||||
savedItemID = webReaderParams?.item?.savedItemId ?: "",
|
||||
labels = it
|
||||
)
|
||||
}
|
||||
coroutineScope.launch {
|
||||
webReaderViewModel.resetBottomSheet()
|
||||
}
|
||||
},
|
||||
onCreateLabel = { newLabelName, labelHexValue ->
|
||||
webReaderViewModel.createNewSavedItemLabel(
|
||||
newLabelName,
|
||||
labelHexValue
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetState.EDIT_INFO -> {
|
||||
BottomSheetUI {
|
||||
EditInfoSheetContent(
|
||||
savedItemId = webReaderParams?.item?.savedItemId,
|
||||
title = webReaderParams?.item?.title,
|
||||
author = webReaderParams?.item?.author,
|
||||
description = webReaderParams?.item?.descriptionText,
|
||||
viewModel = editInfoViewModel,
|
||||
onCancel = {
|
||||
coroutineScope.launch {
|
||||
webReaderViewModel.resetBottomSheet()
|
||||
}
|
||||
},
|
||||
onUpdated = {
|
||||
coroutineScope.launch {
|
||||
webReaderViewModel.updateItemTitle()
|
||||
webReaderViewModel.resetBottomSheet()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetState.LINK -> {
|
||||
BottomSheetUI {
|
||||
OpenLinkView(webReaderViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetState.NONE -> {
|
||||
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
ReaderTopAppBar(webReaderViewModel, onLibraryIconTap)
|
||||
}) { paddingValues ->
|
||||
if (styledContent != null) {
|
||||
WebReader(
|
||||
styledContent = styledContent,
|
||||
webReaderViewModel = webReaderViewModel,
|
||||
currentTheme = currentTheme,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldPopView) {
|
||||
if (shouldPopView) {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReaderTopAppBar(
|
||||
webReaderViewModel: WebReaderViewModel,
|
||||
onLibraryIconTap: (() -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
|
||||
val isDarkMode = isSystemInDarkTheme()
|
||||
val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState()
|
||||
val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value }
|
||||
val toolbarHeightPx: Float by webReaderViewModel.currentToolbarHeightLiveData.observeAsState(
|
||||
0.0f
|
||||
)
|
||||
val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(
|
||||
null
|
||||
)
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val themeBackgroundColor = currentTheme?.let {
|
||||
if (it.themeKey == "System" && isDarkMode) {
|
||||
Color(0xFF000000)
|
||||
} else if (it.themeKey == "System") {
|
||||
Color(0xFFFFFFFF)
|
||||
} else {
|
||||
Color(it.backgroundColor)
|
||||
}
|
||||
} ?: Color(0xFFFFFFFF)
|
||||
|
||||
val themeTintColor = currentTheme?.let {
|
||||
if (it.themeKey == "System" && isDarkMode) {
|
||||
Color(0xFFFFFFFF)
|
||||
} else if (it.themeKey == "System") {
|
||||
Color(0xFF000000)
|
||||
} else {
|
||||
Color(it.foregroundColor)
|
||||
}
|
||||
} ?: Color(0xFF000000)
|
||||
|
||||
|
||||
TopAppBar(
|
||||
modifier = Modifier
|
||||
.height(height = with(LocalDensity.current) {
|
||||
toolbarHeightPx.roundToInt().toDp()
|
||||
}),
|
||||
backgroundColor = themeBackgroundColor,
|
||||
elevation = 0.dp,
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back",
|
||||
tint = themeTintColor
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (onLibraryIconTap != null) {
|
||||
IconButton(onClick = { onLibraryIconTap() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
tint = themeTintColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
webReaderParams?.let {
|
||||
IconButton(onClick = {
|
||||
webReaderViewModel.setBottomSheet(BottomSheetState.NOTEBOOK)
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.notebook),
|
||||
contentDescription = null,
|
||||
tint = themeTintColor
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
webReaderViewModel.setBottomSheet(BottomSheetState.PREFERENCES)
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.format_letter_case),
|
||||
contentDescription = null,
|
||||
tint = themeTintColor
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { isMenuExpanded = true }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dots_horizontal),
|
||||
contentDescription = null,
|
||||
tint = themeTintColor
|
||||
)
|
||||
if (isMenuExpanded) {
|
||||
webReaderParams?.let { params ->
|
||||
SavedItemContextMenu(
|
||||
context = context,
|
||||
isExpanded = true,
|
||||
isArchived = params.item.isArchived,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
webReaderViewModel = webReaderViewModel,
|
||||
actionHandler = {
|
||||
webReaderViewModel.handleSavedItemAction(
|
||||
params.item.savedItemId,
|
||||
it
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomSheetUI(content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp))
|
||||
.background(Color.White)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
Scaffold { paddingValues ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,662 @@
|
|||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.data.archiveSavedItem
|
||||
import app.omnivore.omnivore.core.data.createWebHighlight
|
||||
import app.omnivore.omnivore.core.data.deleteHighlightFromJSON
|
||||
import app.omnivore.omnivore.core.data.deleteSavedItem
|
||||
import app.omnivore.omnivore.core.data.mergeWebHighlights
|
||||
import app.omnivore.omnivore.core.data.unarchiveSavedItem
|
||||
import app.omnivore.omnivore.core.data.updateWebHighlight
|
||||
import app.omnivore.omnivore.core.data.updateWebReadingProgress
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.createNewLabel
|
||||
import app.omnivore.omnivore.core.network.saveUrl
|
||||
import app.omnivore.omnivore.core.network.savedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.feature.components.HighlightColor
|
||||
import app.omnivore.omnivore.feature.library.SavedItemAction
|
||||
import app.omnivore.omnivore.feature.setSavedItemLabels
|
||||
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
data class WebReaderParams(
|
||||
val item: SavedItem,
|
||||
val articleContent: ArticleContent,
|
||||
val labels: List<SavedItemLabel>
|
||||
)
|
||||
|
||||
data class AnnotationWebViewMessage(
|
||||
val annotation: String?
|
||||
)
|
||||
|
||||
enum class Themes(
|
||||
val themeKey: String,
|
||||
val backgroundColor: Long,
|
||||
val foregroundColor: Long,
|
||||
val scrollbarColor: Long
|
||||
) {
|
||||
SYSTEM("System", 0xFF000000, 0xFF000000, 0xFF3A3939),
|
||||
LIGHT("Light", 0xFFFFFFFF, 0xFF000000, 0xFF3A3939),
|
||||
SEPIA("Sepia", 0xFFFBF0D9, 0xFF000000, 0xFF5F4B32),
|
||||
DARK("Dark", 0xFF2F3030, 0xFFFFFFFF, 0xFFD8D7D7),
|
||||
APOLLO("Apollo", 0xFF6A6968, 0xFFFFFFFF, 0xFFF3F3F3),
|
||||
BLACK("Black", 0xFF000000, 0xFFFFFFFF, 0xFFFFFFFF),
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class WebReaderViewModel @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
private val dataService: DataService,
|
||||
private val networker: Networker,
|
||||
private val eventTracker: EventTracker,
|
||||
) : ViewModel() {
|
||||
var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID()
|
||||
var javascriptDispatchQueue: MutableList<String> = mutableListOf()
|
||||
var maxToolbarHeightPx = 0.0f
|
||||
|
||||
val webReaderParamsLiveData = MutableLiveData<WebReaderParams?>(null)
|
||||
var annotation: String? = null
|
||||
val javascriptActionLoopUUIDLiveData = MutableLiveData(lastJavascriptActionLoopUUID)
|
||||
val shouldPopViewLiveData = MutableLiveData(false)
|
||||
val hasFetchError = MutableLiveData(false)
|
||||
val currentToolbarHeightLiveData = MutableLiveData(0.0f)
|
||||
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
|
||||
|
||||
var currentLink: Uri? = null
|
||||
val bottomSheetStateLiveData = MutableLiveData(BottomSheetState.NONE)
|
||||
|
||||
var hasTappedExistingHighlight = false
|
||||
var lastTapCoordinates: TapCoordinates? = null
|
||||
private var isLoading = false
|
||||
private var slug: String? = null
|
||||
|
||||
private val showHighlightColorPalette = MutableLiveData(false)
|
||||
val highlightColor = MutableLiveData(HighlightColor())
|
||||
|
||||
fun loadItem(slug: String?, requestID: String?) {
|
||||
this.slug = slug
|
||||
if (isLoading || webReaderParamsLiveData.value != null) {
|
||||
return
|
||||
}
|
||||
isLoading = true
|
||||
Log.d("reader", "load item called")
|
||||
|
||||
viewModelScope.launch {
|
||||
slug?.let { loadItemUsingSlug(it) }
|
||||
requestID?.let { loadItemUsingRequestID(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun showNavBar() {
|
||||
onScrollChange(maxToolbarHeightPx)
|
||||
}
|
||||
|
||||
fun setBottomSheet(state: BottomSheetState) {
|
||||
bottomSheetStateLiveData.postValue(state)
|
||||
}
|
||||
|
||||
fun resetBottomSheet() {
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.NONE)
|
||||
}
|
||||
|
||||
fun showOpenLinkSheet(context: Context, uri: Uri) {
|
||||
webReaderParamsLiveData.value?.let {
|
||||
if (it.item.pageURLString == uri.toString()) {
|
||||
openLink(context, uri)
|
||||
} else {
|
||||
currentLink = uri
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.LINK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showShareLinkSheet(context: Context) {
|
||||
webReaderParamsLiveData.value?.let {
|
||||
val browserIntent = Intent(Intent.ACTION_SEND)
|
||||
|
||||
browserIntent.setType("text/plain")
|
||||
browserIntent.putExtra(Intent.EXTRA_TEXT, it.item.pageURLString)
|
||||
browserIntent.putExtra(Intent.EXTRA_SUBJECT, it.item.pageURLString)
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
|
||||
fun openCurrentLink(context: Context) {
|
||||
currentLink?.let {
|
||||
openLink(context, it)
|
||||
}
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.NONE)
|
||||
}
|
||||
|
||||
private fun openLink(context: Context, uri: Uri) {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||
startActivity(context, browserIntent, null)
|
||||
}
|
||||
|
||||
fun saveCurrentLink(context: Context) {
|
||||
currentLink?.let {
|
||||
viewModelScope.launch {
|
||||
val success = networker.saveUrl(it)
|
||||
Toast.makeText(
|
||||
context,
|
||||
if (success)
|
||||
context.getString(R.string.web_reader_view_model_save_link_success) else
|
||||
context.getString(R.string.web_reader_view_model_save_link_error),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.NONE)
|
||||
}
|
||||
|
||||
fun copyCurrentLink(context: Context) {
|
||||
currentLink?.let {
|
||||
val clip = ClipData.newPlainText("link", it.toString())
|
||||
val clipboard =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(clip)
|
||||
clipboard.let {
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.web_reader_view_model_copy_link_success),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.NONE)
|
||||
}
|
||||
|
||||
fun onScrollChange(delta: Float) {
|
||||
val newHeight = (currentToolbarHeightLiveData.value ?: 0.0f) + delta
|
||||
currentToolbarHeightLiveData.value = newHeight.coerceIn(0f, maxToolbarHeightPx)
|
||||
}
|
||||
|
||||
private suspend fun loadItemUsingSlug(slug: String) {
|
||||
loadItemFromDB(slug)
|
||||
|
||||
val webReaderParams = loadItemFromServer(slug)
|
||||
|
||||
if (webReaderParams != null) {
|
||||
Log.d("reader", "data loaded from server")
|
||||
eventTracker.track(
|
||||
"link_read",
|
||||
com.posthog.android.Properties()
|
||||
.putValue("linkID", webReaderParams.item.savedItemId)
|
||||
.putValue("slug", webReaderParams.item.slug)
|
||||
.putValue("originalArticleURL", webReaderParams.item.pageURLString)
|
||||
.putValue("loaded_from", "network")
|
||||
)
|
||||
webReaderParamsLiveData.postValue(webReaderParams)
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadItemUsingRequestID(requestID: String, requestCount: Int = 0) {
|
||||
val webReaderParams = loadItemFromServer(requestID)
|
||||
val isSuccessful = webReaderParams?.articleContent?.contentStatus == "SUCCEEDED"
|
||||
|
||||
if (webReaderParams != null && isSuccessful) {
|
||||
this.slug = webReaderParams.item.slug
|
||||
eventTracker.track(
|
||||
"link_read",
|
||||
com.posthog.android.Properties()
|
||||
.putValue("linkID", webReaderParams.item.savedItemId)
|
||||
.putValue("slug", webReaderParams.item.slug)
|
||||
.putValue("originalArticleURL", webReaderParams.item.pageURLString)
|
||||
.putValue("loaded_from", "request_id")
|
||||
)
|
||||
webReaderParamsLiveData.postValue(webReaderParams)
|
||||
isLoading = false
|
||||
} else if (requestCount < 7) {
|
||||
// delay then try again
|
||||
delay(2000L)
|
||||
loadItemUsingRequestID(requestID = requestID, requestCount = requestCount + 1)
|
||||
} else {
|
||||
hasFetchError.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadItemFromDB(slug: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val persistedItem =
|
||||
dataService.db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
|
||||
|
||||
if (persistedItem?.savedItem?.content != null) {
|
||||
val articleContent = ArticleContent(
|
||||
title = persistedItem.savedItem.title,
|
||||
htmlContent = persistedItem.savedItem.content,
|
||||
highlights = persistedItem.highlights,
|
||||
contentStatus = "SUCCEEDED",
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(persistedItem.labels)
|
||||
)
|
||||
|
||||
val webReaderParams = WebReaderParams(
|
||||
persistedItem.savedItem,
|
||||
articleContent,
|
||||
persistedItem.labels
|
||||
)
|
||||
|
||||
Log.d("sync", "data loaded from db")
|
||||
eventTracker.track(
|
||||
"link_read",
|
||||
com.posthog.android.Properties()
|
||||
.putValue("linkID", webReaderParams.item.savedItemId)
|
||||
.putValue("slug", webReaderParams.item.slug)
|
||||
.putValue("originalArticleURL", webReaderParams.item.pageURLString)
|
||||
.putValue("loaded_from", "db")
|
||||
)
|
||||
webReaderParamsLiveData.postValue(webReaderParams)
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadItemFromServer(slug: String): WebReaderParams? {
|
||||
val articleQueryResult = networker.savedItem(slug)
|
||||
|
||||
val article = articleQueryResult.item ?: return null
|
||||
|
||||
val articleContent = ArticleContent(
|
||||
title = article.title,
|
||||
htmlContent = article.content ?: "",
|
||||
highlights = articleQueryResult.highlights,
|
||||
contentStatus = articleQueryResult.state,
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(articleQueryResult.labels)
|
||||
)
|
||||
|
||||
return WebReaderParams(article, articleContent, articleQueryResult.labels)
|
||||
}
|
||||
|
||||
fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
|
||||
when (action) {
|
||||
SavedItemAction.Delete -> {
|
||||
viewModelScope.launch {
|
||||
dataService.deleteSavedItem(itemID)
|
||||
popToLibraryView()
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemAction.Archive -> {
|
||||
viewModelScope.launch {
|
||||
dataService.archiveSavedItem(itemID)
|
||||
popToLibraryView()
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemAction.Unarchive -> {
|
||||
viewModelScope.launch {
|
||||
dataService.unarchiveSavedItem(itemID)
|
||||
popToLibraryView()
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemAction.EditLabels -> {
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.LABELS)
|
||||
}
|
||||
|
||||
SavedItemAction.EditInfo -> {
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.EDIT_INFO)
|
||||
}
|
||||
|
||||
SavedItemAction.MarkRead -> {
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebReadingProgress(
|
||||
jsonString = Gson().toJson(
|
||||
mapOf(
|
||||
"id" to itemID,
|
||||
"readingProgressPercent" to 100.0,
|
||||
"readingProgressAnchorIndex" to 0
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemAction.MarkUnread -> {
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebReadingProgress(
|
||||
jsonString = Gson().toJson(
|
||||
mapOf(
|
||||
"id" to itemID,
|
||||
"readingProgressPercent" to 0,
|
||||
"readingProgressAnchorIndex" to 0
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun popToLibraryView() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
shouldPopViewLiveData.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun showHighlightColorPalette() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
showHighlightColorPalette.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideHighlightColorPalette() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
showHighlightColorPalette.postValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
// fun setHighlightColor(color: HighlightColor) {
|
||||
// CoroutineScope(Dispatchers.Main).launch {
|
||||
// highlightColor.postValue(color)
|
||||
// }
|
||||
// }
|
||||
|
||||
fun handleIncomingWebMessage(actionID: String, jsonString: String) {
|
||||
Log.d("sync", "incoming change: ${actionID}: ${jsonString}")
|
||||
when (actionID) {
|
||||
"createHighlight" -> {
|
||||
viewModelScope.launch {
|
||||
dataService.createWebHighlight(jsonString, highlightColor.value?.name)
|
||||
}
|
||||
}
|
||||
|
||||
"deleteHighlight" -> {
|
||||
viewModelScope.launch {
|
||||
dataService.deleteHighlightFromJSON(jsonString)
|
||||
}
|
||||
}
|
||||
|
||||
"updateHighlight" -> {
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebHighlight(jsonString)
|
||||
}
|
||||
}
|
||||
|
||||
"articleReadingProgress" -> {
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebReadingProgress(jsonString)
|
||||
}
|
||||
}
|
||||
|
||||
"annotate" -> {
|
||||
viewModelScope.launch {
|
||||
val annotationStr = Gson()
|
||||
.fromJson(jsonString, AnnotationWebViewMessage::class.java)
|
||||
.annotation ?: ""
|
||||
annotation = annotationStr
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.HIGHLIGHTNOTE)
|
||||
}
|
||||
}
|
||||
|
||||
"shareHighlight" -> {
|
||||
// unimplemented
|
||||
}
|
||||
|
||||
"mergeHighlight" -> {
|
||||
viewModelScope.launch {
|
||||
dataService.mergeWebHighlights(jsonString)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.d("Loggo", "receive unrecognized action of $actionID with json: $jsonString")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetJavascriptDispatchQueue() {
|
||||
lastJavascriptActionLoopUUID = javascriptActionLoopUUIDLiveData.value ?: UUID.randomUUID()
|
||||
javascriptDispatchQueue = mutableListOf()
|
||||
}
|
||||
|
||||
fun saveAnnotation(annotation: String) {
|
||||
val jsonAnnotation = Gson().toJson(annotation)
|
||||
val script =
|
||||
"var event = new Event('saveAnnotation');event.annotation = $jsonAnnotation;document.dispatchEvent(event);"
|
||||
|
||||
Log.d("loggo", script)
|
||||
|
||||
enqueueScript(script)
|
||||
cancelAnnotationEdit()
|
||||
}
|
||||
|
||||
fun cancelAnnotation() {
|
||||
val script = "var event = new Event('dismissHighlight');document.dispatchEvent(event);"
|
||||
|
||||
enqueueScript(script)
|
||||
cancelAnnotationEdit()
|
||||
}
|
||||
|
||||
private fun cancelAnnotationEdit() {
|
||||
annotation = null
|
||||
resetBottomSheet()
|
||||
}
|
||||
|
||||
private fun enqueueScript(javascript: String) {
|
||||
javascriptDispatchQueue.add(javascript)
|
||||
javascriptActionLoopUUIDLiveData.value = UUID.randomUUID()
|
||||
}
|
||||
|
||||
val currentThemeKey: LiveData<String> = datastoreRepo
|
||||
.themeKeyFlow
|
||||
.distinctUntilChanged()
|
||||
.asLiveData()
|
||||
|
||||
fun storedWebPreferences(isDarkMode: Boolean): WebPreferences = runBlocking {
|
||||
val storedFontSize = datastoreRepo.getInt(DatastoreKeys.preferredWebFontSize)
|
||||
val storedLineHeight = datastoreRepo.getInt(DatastoreKeys.preferredWebLineHeight)
|
||||
val storedMaxWidth = datastoreRepo.getInt(DatastoreKeys.preferredWebMaxWidthPercentage)
|
||||
|
||||
val storedFontFamily =
|
||||
datastoreRepo.getString(DatastoreKeys.preferredWebFontFamily) ?: WebFont.SYSTEM.rawValue
|
||||
val storedThemePreference =
|
||||
datastoreRepo.getString(DatastoreKeys.preferredTheme) ?: "System"
|
||||
val storedWebFont =
|
||||
WebFont.values().firstOrNull { it.rawValue == storedFontFamily } ?: WebFont.values()
|
||||
.first()
|
||||
|
||||
val prefersHighContrastFont =
|
||||
datastoreRepo.getString(DatastoreKeys.prefersWebHighContrastText) == "true"
|
||||
val prefersJustifyText = datastoreRepo.getString(DatastoreKeys.prefersJustifyText) == "true"
|
||||
|
||||
WebPreferences(
|
||||
textFontSize = storedFontSize ?: 12,
|
||||
lineHeight = storedLineHeight ?: 150,
|
||||
maxWidthPercentage = storedMaxWidth ?: 100,
|
||||
themeKey = themeKey(isDarkMode, storedThemePreference),
|
||||
storedThemePreference = storedThemePreference,
|
||||
fontFamily = storedWebFont,
|
||||
prefersHighContrastText = prefersHighContrastFont,
|
||||
prefersJustifyText = prefersJustifyText
|
||||
)
|
||||
}
|
||||
|
||||
fun themeKey(isDarkMode: Boolean, storedThemePreference: String): String {
|
||||
if (storedThemePreference == "System") {
|
||||
return if (isDarkMode) "Black" else "Light"
|
||||
}
|
||||
|
||||
return storedThemePreference
|
||||
}
|
||||
|
||||
fun updateStoredThemePreference(newThemeKey: String) {
|
||||
Log.d("theme", "Setting theme key: $newThemeKey")
|
||||
|
||||
runBlocking {
|
||||
datastoreRepo.putString(DatastoreKeys.preferredTheme, newThemeKey)
|
||||
}
|
||||
|
||||
val script =
|
||||
"var event = new Event('updateTheme');event.themeName = '$newThemeKey';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun setFontSize(newFontSize: Int) {
|
||||
runBlocking {
|
||||
datastoreRepo.putInt(DatastoreKeys.preferredWebFontSize, newFontSize)
|
||||
}
|
||||
val script =
|
||||
"var event = new Event('updateFontSize');event.fontSize = '$newFontSize';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun setMaxWidthPercentage(newMaxWidthPercentageValue: Int) {
|
||||
runBlocking {
|
||||
datastoreRepo.putInt(
|
||||
DatastoreKeys.preferredWebMaxWidthPercentage,
|
||||
newMaxWidthPercentageValue
|
||||
)
|
||||
}
|
||||
val script =
|
||||
"var event = new Event('updateMaxWidthPercentage');event.maxWidthPercentage = '$newMaxWidthPercentageValue';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun setLineHeight(newLineHeight: Int) {
|
||||
runBlocking {
|
||||
datastoreRepo.putInt(DatastoreKeys.preferredWebLineHeight, newLineHeight)
|
||||
}
|
||||
val script =
|
||||
"var event = new Event('updateLineHeight');event.lineHeight = '$newLineHeight';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun updateHighContrastTextPreference(prefersHighContrastText: Boolean) {
|
||||
runBlocking {
|
||||
datastoreRepo.putString(
|
||||
DatastoreKeys.prefersWebHighContrastText,
|
||||
prefersHighContrastText.toString()
|
||||
)
|
||||
}
|
||||
val fontContrastValue = if (prefersHighContrastText) "high" else "normal"
|
||||
val script =
|
||||
"var event = new Event('handleFontContrastChange');event.fontContrast = '$fontContrastValue';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun updateJustifyText(justifyText: Boolean) {
|
||||
runBlocking {
|
||||
datastoreRepo.putString(DatastoreKeys.prefersJustifyText, justifyText.toString())
|
||||
}
|
||||
val script =
|
||||
"var event = new Event('updateJustifyText');event.justifyText = $justifyText;document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun applyWebFont(font: WebFont) {
|
||||
runBlocking {
|
||||
datastoreRepo.putString(DatastoreKeys.preferredWebFontFamily, font.rawValue)
|
||||
}
|
||||
|
||||
val script =
|
||||
"var event = new Event('updateFontFamily');event.fontFamily = '${font.rawValue}';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
setSavedItemLabels(
|
||||
networker = networker,
|
||||
dataService = dataService,
|
||||
savedItemID = savedItemID,
|
||||
labels = labels
|
||||
)
|
||||
|
||||
slug?.let {
|
||||
loadItemFromDB(it)
|
||||
}
|
||||
|
||||
// Send labels to webview
|
||||
val script =
|
||||
"var event = new Event('updateLabels');event.labels = ${Gson().toJson(labels)};document.dispatchEvent(event);"
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
enqueueScript(script)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateItemTitle() {
|
||||
viewModelScope.launch {
|
||||
slug?.let {
|
||||
loadItemFromDB(it)
|
||||
}
|
||||
|
||||
webReaderParamsLiveData.value?.item?.title?.let {
|
||||
updateItemTitleInWebView(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateItemTitleInWebView(title: String) {
|
||||
val script =
|
||||
"var event = new Event('updateTitle');event.title = '${title}';document.dispatchEvent(event);"
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
enqueueScript(script)
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewSavedItemLabel(labelName: String, hexColorValue: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
val newLabel = networker.createNewLabel(
|
||||
CreateLabelInput(
|
||||
color = presentIfNotNull(hexColorValue),
|
||||
name = labelName
|
||||
)
|
||||
)
|
||||
|
||||
newLabel?.let {
|
||||
val savedItemLabel = SavedItemLabel(
|
||||
savedItemLabelId = it.id,
|
||||
name = it.name,
|
||||
color = it.color,
|
||||
createdAt = it.createdAt as String?,
|
||||
labelDescription = it.description
|
||||
)
|
||||
|
||||
dataService.db.savedItemLabelDao().insertAll(listOf(savedItemLabel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.root
|
||||
package app.omnivore.omnivore.feature.root
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -8,19 +8,19 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.ui.auth.WelcomeScreen
|
||||
import app.omnivore.omnivore.ui.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.ui.editinfo.EditInfoViewModel
|
||||
import app.omnivore.omnivore.ui.library.LibraryView
|
||||
import app.omnivore.omnivore.ui.library.LibraryViewModel
|
||||
import app.omnivore.omnivore.ui.library.SearchView
|
||||
import app.omnivore.omnivore.ui.library.SearchViewModel
|
||||
import app.omnivore.omnivore.ui.save.SaveViewModel
|
||||
import app.omnivore.omnivore.ui.settings.PolicyWebView
|
||||
import app.omnivore.omnivore.ui.settings.SettingsView
|
||||
import app.omnivore.omnivore.ui.settings.SettingsViewModel
|
||||
import app.omnivore.omnivore.navigation.Routes
|
||||
import app.omnivore.omnivore.feature.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.feature.auth.WelcomeScreen
|
||||
import app.omnivore.omnivore.feature.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
|
||||
import app.omnivore.omnivore.feature.library.LibraryView
|
||||
import app.omnivore.omnivore.feature.library.LibraryViewModel
|
||||
import app.omnivore.omnivore.feature.library.SearchView
|
||||
import app.omnivore.omnivore.feature.library.SearchViewModel
|
||||
import app.omnivore.omnivore.feature.save.SaveViewModel
|
||||
import app.omnivore.omnivore.feature.settings.PolicyWebView
|
||||
import app.omnivore.omnivore.feature.settings.SettingsView
|
||||
import app.omnivore.omnivore.feature.settings.SettingsViewModel
|
||||
|
||||
@Composable
|
||||
fun RootView(
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.save
|
||||
package app.omnivore.omnivore.feature.save
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -16,7 +16,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity
|
||||
import app.omnivore.omnivore.feature.reader.WebReaderLoadingContainerActivity
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.save
|
||||
package app.omnivore.omnivore.feature.save
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
|
|
@ -12,7 +12,6 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.save
|
||||
package app.omnivore.omnivore.feature.save
|
||||
|
||||
import android.util.Patterns
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -8,13 +8,13 @@ import androidx.compose.ui.text.intl.Locale
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.Constants
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.utils.Constants
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.graphql.generated.SaveUrlMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.SaveUrlInput
|
||||
import app.omnivore.omnivore.ui.ResourceProvider
|
||||
import app.omnivore.omnivore.feature.ResourceProvider
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
package app.omnivore.omnivore.feature.savedItemViews
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.compose.ui.text.toUpperCase
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.feature.components.LabelChip
|
||||
import app.omnivore.omnivore.feature.components.LabelChipColors
|
||||
import app.omnivore.omnivore.feature.library.SavedItemAction
|
||||
import app.omnivore.omnivore.feature.library.SavedItemViewModel
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun SavedItemCard(
|
||||
selected: Boolean,
|
||||
savedItemViewModel: SavedItemViewModel,
|
||||
savedItem: SavedItemWithLabelsAndHighlights,
|
||||
onClickHandler: () -> Unit,
|
||||
actionHandler: (SavedItemAction) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.combinedClickable(onClick = onClickHandler, onLongClick = {
|
||||
savedItemViewModel.actionsMenuItemLiveData.postValue(savedItem)
|
||||
})
|
||||
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.background)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
.background(Color.Transparent)
|
||||
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.padding(end = 20.dp)
|
||||
.defaultMinSize(minHeight = 55.dp)
|
||||
) {
|
||||
ReadInfo(item = savedItem)
|
||||
|
||||
Text(
|
||||
text = savedItem.savedItem.title, style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
), maxLines = 2, lineHeight = 20.sp
|
||||
)
|
||||
|
||||
if (savedItem.savedItem.author != null && savedItem.savedItem.author != "") {
|
||||
Text(
|
||||
text = byline(savedItem), style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
), maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(savedItem.savedItem.imageURLString),
|
||||
contentDescription = "Image associated with saved item",
|
||||
modifier = Modifier
|
||||
.size(55.dp, 73.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.defaultMinSize(minWidth = 55.dp, minHeight = 73.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
)
|
||||
}
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
) {
|
||||
savedItem.labels.filter { !isFlairLabel(it) }
|
||||
.sortedWith(compareBy { it.name.toLowerCase(Locale.current) }).forEach { label ->
|
||||
val chipColors = LabelChipColors.fromHex(label.color)
|
||||
|
||||
LabelChip(
|
||||
modifier = Modifier.clickable { }, name = label.name, colors = chipColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp)
|
||||
}
|
||||
}
|
||||
|
||||
fun byline(item: SavedItemWithLabelsAndHighlights): String {
|
||||
item.savedItem.author?.let {
|
||||
return item.savedItem.author
|
||||
}
|
||||
|
||||
val publisherDisplayName = item.savedItem.publisherDisplayName()
|
||||
publisherDisplayName?.let {
|
||||
return publisherDisplayName
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
fun estimatedReadingTime(item: SavedItemWithLabelsAndHighlights): String {
|
||||
item.savedItem.wordsCount?.let {
|
||||
if (it > 0) {
|
||||
val readLen = kotlin.math.max(1, it / 235)
|
||||
return "$readLen MIN READ • "
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fun readingProgress(item: SavedItemWithLabelsAndHighlights): String {
|
||||
// If there is no wordsCount don't show progress because it will make no sense
|
||||
item.savedItem.wordsCount?.let {
|
||||
if (it > 0) {
|
||||
val intVal = item.savedItem.readingProgress.toInt()
|
||||
return "$intVal%"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
//var highlightsText: String {
|
||||
// item.hig ?.let {
|
||||
// if let highlights = item.highlights, highlights.count > 0 {
|
||||
// let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count)
|
||||
// if item.wordsCount > 0 {
|
||||
// return " • \(fmted)"
|
||||
// }
|
||||
// return fmted
|
||||
// }
|
||||
// return ""
|
||||
//}
|
||||
//
|
||||
//var notesText: String {
|
||||
// let notes = item.highlights?.filter { item in
|
||||
// if let highlight = item as? Highlight {
|
||||
// return !(highlight.annotation ?? "").isEmpty
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// if let notes = notes, notes.count > 0 {
|
||||
// let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count)
|
||||
// if item.wordsCount > 0 {
|
||||
// return " • \(fmted)"
|
||||
// }
|
||||
// return fmted
|
||||
// }
|
||||
// return ""
|
||||
//}
|
||||
|
||||
|
||||
enum class FlairIcon(
|
||||
val rawValue: String, val sortOrder: Int
|
||||
) {
|
||||
FEED("feed", 0), RSS("rss", 0), FAVORITE("favorite", 1), NEWSLETTER(
|
||||
"newsletter", 2
|
||||
),
|
||||
RECOMMENDED("recommended", 3), PINNED("pinned", 4)
|
||||
}
|
||||
|
||||
val FLAIR_ICON_NAMES = listOf("feed", "rss", "favorite", "newsletter", "recommended", "pinned")
|
||||
|
||||
fun isFlairLabel(label: SavedItemLabel): Boolean {
|
||||
return FLAIR_ICON_NAMES.contains(label.name.toLowerCase(Locale.current))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun flairIcons(item: SavedItemWithLabelsAndHighlights) {
|
||||
val labels = item.labels.filter { isFlairLabel(it) }.map {
|
||||
FlairIcon.valueOf(it.name.toUpperCase(Locale.current))
|
||||
}
|
||||
labels.forEach {
|
||||
when (it) {
|
||||
FlairIcon.RSS, FlairIcon.FEED -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flair_feed),
|
||||
contentDescription = "Feed flair Icon",
|
||||
modifier = Modifier.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
FlairIcon.FAVORITE -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flaire_favorite),
|
||||
contentDescription = "Favorite flair Icon",
|
||||
modifier = Modifier.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
FlairIcon.NEWSLETTER -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flair_newsletter),
|
||||
contentDescription = "Newsletter flair Icon",
|
||||
modifier = Modifier.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
FlairIcon.RECOMMENDED -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flair_recommended),
|
||||
contentDescription = "Recommended flair Icon",
|
||||
modifier = Modifier.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
FlairIcon.PINNED -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.flair_pinned),
|
||||
contentDescription = "Pinned flair Icon",
|
||||
modifier = Modifier.padding(end = 5.0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReadInfo(item: SavedItemWithLabelsAndHighlights) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 15.dp)
|
||||
) {
|
||||
// Show flair here
|
||||
flairIcons(item)
|
||||
|
||||
Text(
|
||||
text = estimatedReadingTime(item), style = TextStyle(
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
), maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = readingProgress(item), style = TextStyle(
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (item.savedItem.readingProgress > 1) colorResource(R.color.green_55B938) else colorResource(
|
||||
R.color.gray_898989
|
||||
)
|
||||
), maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package app.omnivore.omnivore.feature.savedItemViews
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.library.SavedItemAction
|
||||
import app.omnivore.omnivore.feature.reader.WebReaderViewModel
|
||||
|
||||
@Composable
|
||||
fun SavedItemContextMenu(
|
||||
isExpanded: Boolean,
|
||||
isArchived: Boolean,
|
||||
context: Context,
|
||||
webReaderViewModel: WebReaderViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
actionHandler: (SavedItemAction) -> Unit
|
||||
) {
|
||||
val menuOptions = listOf(
|
||||
MenuItemOption(R.string.saved_item_context_menu_action_edit_info, SavedItemAction.EditInfo),
|
||||
MenuItemOption(
|
||||
R.string.saved_item_context_menu_action_edit_labels,
|
||||
SavedItemAction.EditLabels
|
||||
),
|
||||
MenuItemOption(
|
||||
textResourceId = if (isArchived) R.string.saved_item_context_menu_action_unarchive else R.string.saved_item_context_menu_action_archive,
|
||||
action = if (isArchived) SavedItemAction.Unarchive else SavedItemAction.Archive
|
||||
),
|
||||
MenuItemOption(
|
||||
textResourceId = R.string.saved_item_context_menu_action_share_original,
|
||||
customAction = {
|
||||
webReaderViewModel.showShareLinkSheet(context)
|
||||
onDismiss()
|
||||
}
|
||||
),
|
||||
MenuItemOption(R.string.saved_item_context_menu_action_remove_item, SavedItemAction.Delete)
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
menuOptions.forEach { option ->
|
||||
if (option.customAction != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text( text = stringResource(option.textResourceId), fontWeight = FontWeight.Normal) },
|
||||
onClick = {
|
||||
option.customAction.invoke()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text( text = stringResource(option.textResourceId), fontWeight = FontWeight.Normal) },
|
||||
onClick = {
|
||||
option.action?.let { actionHandler(it) }
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MenuItemOption(
|
||||
val textResourceId: Int,
|
||||
val action: SavedItemAction? = null,
|
||||
val customAction: (() -> Unit)? = null
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.savedItemViews
|
||||
package app.omnivore.omnivore.feature.savedItemViews
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -10,8 +10,8 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import app.omnivore.omnivore.persistence.entities.TypeaheadCardData
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import app.omnivore.omnivore.core.database.entities.TypeaheadCardData
|
||||
import app.omnivore.omnivore.feature.library.SavedItemAction
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.settings
|
||||
package app.omnivore.omnivore.feature.settings
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package app.omnivore.omnivore.feature.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@Composable
|
||||
fun ManageAccountDialog(onDismiss: () -> Unit, settingsViewModel: SettingsViewModel) {
|
||||
Dialog(onDismissRequest = { onDismiss() }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.height(300.dp)
|
||||
) {
|
||||
ManageAccountView(settingsViewModel = settingsViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ManageAccountView(settingsViewModel: SettingsViewModel) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp, bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(stringResource(R.string.manage_account_title))
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { settingsViewModel.resetDataCache() })
|
||||
) {
|
||||
Text(stringResource(R.string.manage_account_action_reset_data_cache))
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Icon(imageVector = Icons.Filled.Refresh, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.omnivore.omnivore.ui.settings
|
||||
package app.omnivore.omnivore.feature.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
|
|
@ -14,7 +14,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.navigation.Routes
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "SetJavaScriptEnabled")
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue