diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 4bf7ef0f3..9315da3bf 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -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 { diff --git a/android/Omnivore/app/src/main/AndroidManifest.xml b/android/Omnivore/app/src/main/AndroidManifest.xml index 4ebf91f5d..dce141df0 100644 --- a/android/Omnivore/app/src/main/AndroidManifest.xml +++ b/android/Omnivore/app/src/main/AndroidManifest.xml @@ -31,7 +31,7 @@ @@ -47,12 +47,12 @@ android:windowSoftInputMode="adjustNothing" /> diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/EventTracker.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/EventTracker.kt deleted file mode 100644 index 38abeeda0..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/EventTracker.kt +++ /dev/null @@ -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() - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt index 8996416ad..b6e878e04 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt deleted file mode 100644 index 01b4b9a6f..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt +++ /dev/null @@ -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") -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/analytics/EventTracker.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/analytics/EventTracker.kt new file mode 100644 index 000000000..4f5a95898 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/analytics/EventTracker.kt @@ -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() + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/DataService.kt similarity index 73% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/DataService.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/DataService.kt index 306501332..921eaeed2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/DataService.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/HighlightActionHandlers.kt similarity index 90% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/HighlightActionHandlers.kt index a7f74ffb4..bc2d8e9a7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/HighlightActionHandlers.kt @@ -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) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt similarity index 90% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt index 44a0dbaf4..dabe14122 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt @@ -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) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/NanoId.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/NanoId.kt similarity index 99% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/NanoId.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/NanoId.kt index 925c8f498..71d5e2d4a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/NanoId.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/NanoId.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.dataService +package app.omnivore.omnivore.core.data import java.security.SecureRandom diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/ReadingProgressChangeHandler.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/ReadingProgressChangeHandler.kt new file mode 100644 index 000000000..4f06bc07e --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/ReadingProgressChangeHandler.kt @@ -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) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemLabelSync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SavedItemLabelSync.kt similarity index 59% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemLabelSync.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SavedItemLabelSync.kt index 95d59e0bb..8ad37f25e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemLabelSync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SavedItemLabelSync.kt @@ -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() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SavedItemMenuActionHandlers.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SavedItemMenuActionHandlers.kt new file mode 100644 index 000000000..471e5afb7 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SavedItemMenuActionHandlers.kt @@ -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) + } + } +} + diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt similarity index 89% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt index 4ec2fac43..c818af698 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt @@ -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") diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/AppDatabase.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/AppDatabase.kt new file mode 100644 index 000000000..188787184 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/AppDatabase.kt @@ -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 +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/BaseDao.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/BaseDao.kt similarity index 94% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/BaseDao.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/BaseDao.kt index e422079ae..2b5bcbc7f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/BaseDao.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/BaseDao.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.persistence +package app.omnivore.omnivore.core.database import androidx.room.Delete import androidx.room.Insert diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt index 8d133ad89..6d52e6203 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/HighlightChange.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt similarity index 95% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/HighlightChange.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt index b2b5e0bfb..33be6c46c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/HighlightChange.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt @@ -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) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/NewsletterEmail.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/NewsletterEmail.kt similarity index 79% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/NewsletterEmail.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/NewsletterEmail.kt index c9b38351e..5d52fc636 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/NewsletterEmail.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/NewsletterEmail.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecentSearchItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/RecentSearchItem.kt similarity index 78% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecentSearchItem.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/RecentSearchItem.kt index ec5f41bb4..e45912374 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecentSearchItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/RecentSearchItem.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Recommendation.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Recommendation.kt similarity index 82% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Recommendation.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Recommendation.kt index 2c41c9473..ed5b612fd 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Recommendation.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Recommendation.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecommendationGroup.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/RecommendationGroup.kt similarity index 87% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecommendationGroup.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/RecommendationGroup.kt index f168922f0..e635b8b05 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecommendationGroup.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/RecommendationGroup.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt new file mode 100644 index 000000000..33df145a5 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt @@ -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) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertLabelCrossRefs(items: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertLabels(items: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertHighlights(items: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertHighlightCrossRefs(items: List) + + @Transaction + open fun insertAll(savedItems: List) { + insertSavedItems(savedItems.map { it.savedItem }) + + val labels: MutableList = mutableListOf() + val highlights: MutableList = mutableListOf() + + val labelCrossRefs: MutableList = mutableListOf() + val highlightCrossRefs: MutableList = 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 + + @Query("SELECT * FROM savedItem WHERE savedItemId = :itemID") + fun findById(itemID: String): SavedItem? + + @Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0") + fun getUnSynced(): List + + @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) + + @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 + + @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, + sortKey: String, + hasRequiredLabels: Int, + hasExcludedLabels: Int, + requiredLabels: List, + excludedLabels: List, + allowedContentReaders: List + ): LiveData> + + fun filteredLibraryData( + allowedArchiveStates: List, + sortKey: String, + requiredLabels: List, + excludedLabels: List, + allowedContentReaders: List + ): LiveData> { + 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 " +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt similarity index 94% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt index 47e6f513f..485238ff5 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt @@ -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( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/UserProfile.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/UserProfile.kt similarity index 79% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/UserProfile.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/UserProfile.kt index 12408f40c..48866e307 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/UserProfile.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/UserProfile.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Viewer.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Viewer.kt similarity index 84% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Viewer.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Viewer.kt index e323a8257..a2c64b70c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Viewer.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Viewer.kt @@ -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( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/datastore/DatastoreRepository.kt similarity index 94% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/datastore/DatastoreRepository.kt index 9fa050fa4..1d9a6fa86 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/datastore/DatastoreRepository.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/model/ServerSyncStatus.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/model/ServerSyncStatus.kt new file mode 100644 index 000000000..2bbb6c930 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/model/ServerSyncStatus.kt @@ -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) +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/HighlightMutations.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/HighlightMutations.kt index 72d3eeb64..359137fbb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/HighlightMutations.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/Networker.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/Networker.kt similarity index 77% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/Networker.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/Networker.kt index 4b6a811d3..6a2194b96 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/Networker.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/Networker.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/RESTNetworker.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/RESTNetworker.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/RESTNetworker.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/RESTNetworker.kt index f1ade8dbd..b82c64c44 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/RESTNetworker.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/RESTNetworker.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/ReadingProgressMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ReadingProgressMutations.kt similarity index 97% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/ReadingProgressMutations.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ReadingProgressMutations.kt index 2ca820474..1e33d801b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/ReadingProgressMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ReadingProgressMutations.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelMutations.kt similarity index 95% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelMutations.kt index e4dd3561f..a1d7df7d1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelMutations.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelQuery.kt similarity index 85% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelQuery.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelQuery.kt index 521249da6..27c21a8aa 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelQuery.kt @@ -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 { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemMutations.kt new file mode 100644 index 000000000..eb55da347 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemMutations.kt @@ -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 + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemQuery.kt similarity index 94% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemQuery.kt index cd2f50e35..856b1d8ea 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemQuery.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemUpdatesQuery.kt similarity index 93% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemUpdatesQuery.kt index 783ff3656..d195e7cd5 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemUpdatesQuery.kt @@ -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( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt similarity index 92% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt index 018dce283..31664e652 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt @@ -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( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/TypeaheadSearch.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/TypeaheadSearch.kt similarity index 88% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/TypeaheadSearch.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/TypeaheadSearch.kt index 1091c2889..4b6f70cd6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/TypeaheadSearch.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/TypeaheadSearch.kt @@ -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?, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/ViewerQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ViewerQuery.kt similarity index 83% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/ViewerQuery.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ViewerQuery.kt index 8a4118504..a9e16957e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/ViewerQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ViewerQuery.kt @@ -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 { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt deleted file mode 100644 index 98e175ddd..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt +++ /dev/null @@ -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) - } - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemMenuActionHandlers.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemMenuActionHandlers.kt deleted file mode 100644 index 10988a3cc..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemMenuActionHandlers.kt +++ /dev/null @@ -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) - } - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/AppModule.kt similarity index 71% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/AppModule.kt index 1bf33125d..70de06127 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/AppModule.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/LabelUtils.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/LabelUtils.kt similarity index 84% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/LabelUtils.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/LabelUtils.kt index 8b6323820..0826d78b2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/LabelUtils.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/LabelUtils.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/ResourceProvider.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/ResourceProvider.kt similarity index 91% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/ResourceProvider.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/ResourceProvider.kt index 192486f16..9e5ed576c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/ResourceProvider.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/ResourceProvider.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.ui +package app.omnivore.omnivore.feature import android.content.Context import androidx.annotation.StringRes diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AppleAuth.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/AppleAuth.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AppleAuth.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/AppleAuth.kt index 040ce7762..16c8e5227 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AppleAuth.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/AppleAuth.kt @@ -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.* diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AuthUtils.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/AuthUtils.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AuthUtils.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/AuthUtils.kt index c51f93b50..f5ad49c98 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AuthUtils.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/AuthUtils.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/CreateUserProfile.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/CreateUserProfile.kt similarity index 99% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/CreateUserProfile.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/CreateUserProfile.kt index 276ec6cab..43b1e7413 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/CreateUserProfile.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/CreateUserProfile.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.ui.auth +package app.omnivore.omnivore.feature.auth import android.annotation.SuppressLint import android.widget.Toast diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/EmailLogin.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/EmailLogin.kt index 67ed205c2..5f9f4d57e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/EmailLogin.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailSignUpView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/EmailSignUpView.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailSignUpView.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/EmailSignUpView.kt index 268aad3dc..d8ad8b808 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailSignUpView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/EmailSignUpView.kt @@ -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) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/GoogleAuth.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/GoogleAuth.kt index 46055919d..bee519a98 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/GoogleAuth.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoadingButtonWithIcon.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/LoadingButtonWithIcon.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoadingButtonWithIcon.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/LoadingButtonWithIcon.kt index 5593e029c..2c1a9f136 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoadingButtonWithIcon.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/LoadingButtonWithIcon.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/LoginViewModel.kt similarity index 91% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/LoginViewModel.kt index ff9b40868..7ced32d09 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/LoginViewModel.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/SelfHostedView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/SelfHostedView.kt similarity index 93% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/SelfHostedView.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/SelfHostedView.kt index bf8389434..616c3c7e7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/SelfHostedView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/SelfHostedView.kt @@ -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") diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/WelcomeScreen.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/WelcomeScreen.kt index 45dfe5095..38f2c156d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/auth/WelcomeScreen.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/AddLinkSheet.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/AddLinkSheet.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/AddLinkSheet.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/AddLinkSheet.kt index 1174bb6c3..acbfe48cd 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/AddLinkSheet.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/AddLinkSheet.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColor.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColor.kt similarity index 75% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColor.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColor.kt index 666489828..eb5adb9b3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColor.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColor.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.ui.components +package app.omnivore.omnivore.feature.components import androidx.compose.ui.graphics.Color diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColorPalette.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColorPalette.kt similarity index 97% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColorPalette.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColorPalette.kt index 8fc4aee03..d2167c039 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColorPalette.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColorPalette.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColorPaletteItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColorPaletteItem.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColorPaletteItem.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColorPaletteItem.kt index 8f00fed31..c267d373e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColorPaletteItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColorPaletteItem.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColorPaletteMode.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColorPaletteMode.kt similarity index 76% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColorPaletteMode.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColorPaletteMode.kt index 29eca575d..9b775060b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/HighlightColorPaletteMode.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/HighlightColorPaletteMode.kt @@ -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) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChip.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelChip.kt similarity index 94% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChip.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelChip.kt index a7342b9d9..837f9f5dc 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChip.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelChip.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChipColors.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelChipColors.kt similarity index 90% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChipColors.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelChipColors.kt index 2ee6b8434..98827dac9 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChipColors.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelChipColors.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelCreationDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelCreationDialog.kt similarity index 99% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelCreationDialog.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelCreationDialog.kt index fcadcdaea..0f34953ee 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelCreationDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelCreationDialog.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsSelectionSheet.kt similarity index 80% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsSelectionSheet.kt index 9f3ca3606..5dbb3b321 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsSelectionSheet.kt @@ -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 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, @@ -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) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsViewModel.kt similarity index 88% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsViewModel.kt index 9b8ba18b1..ecc1bc741 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsViewModel.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/SegmentedControl.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/SegmentedControl.kt index 2ebf45e94..e21008710 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/SegmentedControl.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/editinfo/EditInfoSheet.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/editinfo/EditInfoSheet.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/editinfo/EditInfoSheet.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/editinfo/EditInfoSheet.kt index ad2b04f1b..799419970 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/editinfo/EditInfoSheet.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/editinfo/EditInfoSheet.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/editinfo/EditInfoViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/editinfo/EditInfoViewModel.kt similarity index 91% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/editinfo/EditInfoViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/editinfo/EditInfoViewModel.kt index a27bc4817..befbe8f67 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/editinfo/EditInfoViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/editinfo/EditInfoViewModel.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryBottomSheetState.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryBottomSheetState.kt similarity index 65% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryBottomSheetState.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryBottomSheetState.kt index 4bd866b45..f2c907d06 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryBottomSheetState.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryBottomSheetState.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.ui.library +package app.omnivore.omnivore.feature.library enum class LibraryBottomSheetState { HIDDEN, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt index 576b21e9d..4c9c317ec 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt @@ -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) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryNavigationBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryNavigationBar.kt new file mode 100644 index 000000000..a65973a40 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryNavigationBar.kt @@ -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() + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt similarity index 95% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt index 32e0d66a4..c45230d77 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt similarity index 82% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt index 3f7f54e34..e79684c4f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt @@ -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(capacity = Channel.UNLIMITED) + private val contentRequestChannel = Channel(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 } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemFilter.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemFilter.kt similarity index 97% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemFilter.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemFilter.kt index 503a261bf..c15b0fd6e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemFilter.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemFilter.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemLibraryContextMenu.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemLibraryContextMenu.kt new file mode 100644 index 000000000..44d6af865 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemLibraryContextMenu.kt @@ -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 +) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemSortFilter.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemSortFilter.kt new file mode 100644 index 000000000..ab0641d35 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemSortFilter.kt @@ -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() + } + ) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemViewModel.kt similarity index 70% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemViewModel.kt index 199c668ce..559caa299 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemViewModel.kt @@ -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 { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SearchView.kt new file mode 100644 index 000000000..3df01dea5 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SearchView.kt @@ -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 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 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 + ) + } + ) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SearchViewModel.kt similarity index 71% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SearchViewModel.kt index b9e3bd839..1ad2360cb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SearchViewModel.kt @@ -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(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) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt index 0780d0f84..6e381e7d8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookViewModel.kt similarity index 82% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookViewModel.kt index fd4763f8a..a785b5886 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookViewModel.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/AnnotationEditView.kt similarity index 91% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/AnnotationEditView.kt index 03a4c3fdf..85bc28d38 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/AnnotationEditView.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/OpenLinkView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/OpenLinkView.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/OpenLinkView.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/OpenLinkView.kt index c658d4b27..b97b7030e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/OpenLinkView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/OpenLinkView.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/PDFReader.kt similarity index 99% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/PDFReader.kt index 8763fa5c3..b8721e05c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/PDFReader.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/PDFReaderViewModel.kt similarity index 92% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/PDFReaderViewModel.kt index d1efea02a..90f0b1839 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/PDFReaderViewModel.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/ReaderPreferencesView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/ReaderPreferencesView.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/ReaderPreferencesView.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/ReaderPreferencesView.kt index 40e967f22..5505cd6bb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/ReaderPreferencesView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/ReaderPreferencesView.kt @@ -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) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReader.kt new file mode 100644 index 000000000..988565701 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReader.kt @@ -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?) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderContent.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderContent.kt index 79f8fbeb3..2616303ff 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderContent.kt @@ -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) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderLoadingContainer.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderLoadingContainer.kt new file mode 100644 index 000000000..98f7230f6 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderLoadingContainer.kt @@ -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(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 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() + } + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt new file mode 100644 index 000000000..43848ceeb --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt @@ -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 +) + +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 = mutableListOf() + var maxToolbarHeightPx = 0.0f + + val webReaderParamsLiveData = MutableLiveData(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 = 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) { + 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)) + } + } + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt similarity index 79% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt index cdb12f527..42643e2f3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt @@ -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( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveContent.kt similarity index 95% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveContent.kt index 203f41700..8d6f352b4 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveContent.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveSheetActivity.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveSheetActivity.kt index 42d9f83e8..f1e6dbf1e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveSheetActivity.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveViewModel.kt similarity index 93% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveViewModel.kt index 437982b0b..f2dba6cf7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveViewModel.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemCard.kt new file mode 100644 index 000000000..f8fd58f3e --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemCard.kt @@ -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 + ) + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemContextMenu.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemContextMenu.kt new file mode 100644 index 000000000..de7278973 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemContextMenu.kt @@ -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 +) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/TypeaheadSearchCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/TypeaheadSearchCard.kt similarity index 90% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/TypeaheadSearchCard.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/TypeaheadSearchCard.kt index 551594bd1..2eb86e04a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/TypeaheadSearchCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/TypeaheadSearchCard.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/LogoutDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/LogoutDialog.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/LogoutDialog.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/LogoutDialog.kt index 569990988..993944f8f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/LogoutDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/LogoutDialog.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/ManageAccount.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/ManageAccount.kt new file mode 100644 index 000000000..daeec8318 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/ManageAccount.kt @@ -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) + } + } + } +} + diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/PolicyWebView.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/PolicyWebView.kt index 71553eb41..b7f7d6d8f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/PolicyWebView.kt @@ -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") diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsContent.kt similarity index 97% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsContent.kt index 56de10295..a2e34294d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsContent.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.ui.settings +package app.omnivore.omnivore.feature.settings import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -18,8 +18,8 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import app.omnivore.omnivore.BuildConfig import app.omnivore.omnivore.R -import app.omnivore.omnivore.Routes -import app.omnivore.omnivore.ui.auth.LoginViewModel +import app.omnivore.omnivore.navigation.Routes +import app.omnivore.omnivore.feature.auth.LoginViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsViewModel.kt similarity index 75% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsViewModel.kt index 573a01f1b..6dde3d23f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsViewModel.kt @@ -1,12 +1,12 @@ -package app.omnivore.omnivore.ui.settings +package app.omnivore.omnivore.feature.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.omnivore.omnivore.DatastoreKeys -import app.omnivore.omnivore.DatastoreRepository -import app.omnivore.omnivore.dataService.DataService -import app.omnivore.omnivore.networking.Networker -import app.omnivore.omnivore.networking.viewer +import app.omnivore.omnivore.utils.DatastoreKeys +import app.omnivore.omnivore.core.datastore.DatastoreRepository +import app.omnivore.omnivore.core.data.DataService +import app.omnivore.omnivore.core.network.Networker +import app.omnivore.omnivore.core.network.viewer import dagger.hilt.android.lifecycle.HiltViewModel import io.intercom.android.sdk.Intercom import io.intercom.android.sdk.IntercomSpace diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Color.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Color.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Color.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Color.kt index 8939caf19..13e96a379 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Color.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Color.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.ui.theme +package app.omnivore.omnivore.feature.theme import androidx.compose.ui.graphics.Color diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Shape.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Shape.kt similarity index 85% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Shape.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Shape.kt index a8b1533e7..15ff7fd39 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Shape.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Shape.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.ui.theme +package app.omnivore.omnivore.feature.theme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Shapes diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Theme.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Theme.kt index 9384a2ce8..2b795260d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Theme.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.ui.theme +package app.omnivore.omnivore.feature.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Type.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Type.kt similarity index 96% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Type.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Type.kt index 86df5fe88..b9786c14a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Type.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/theme/Type.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.ui.theme +package app.omnivore.omnivore.feature.theme import androidx.compose.material3.Typography diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt deleted file mode 100644 index 46871ee93..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt +++ /dev/null @@ -1,10 +0,0 @@ -package app.omnivore.omnivore.models - -enum class ServerSyncStatus(val rawValue: Int) { - IS_SYNCED(0), - IS_SYNCING(1), - NEEDS_DELETION(2), - NEEDS_CREATION(3), - NEEDS_UPDATE(4), - NEEDS_MERGE(5) -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt new file mode 100644 index 000000000..62f1a780b --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt @@ -0,0 +1,11 @@ +package app.omnivore.omnivore.navigation + +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") +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt deleted file mode 100644 index 20ee0a878..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt +++ /dev/null @@ -1,68 +0,0 @@ -package app.omnivore.omnivore.networking - -import android.content.ContentValues -import android.net.Uri -import android.util.Log -import androidx.compose.ui.text.intl.Locale -import androidx.lifecycle.viewModelScope -import app.omnivore.omnivore.Constants -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.* -import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.api.Optional -import kotlinx.coroutines.launch -import java.util.* - -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 - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt deleted file mode 100644 index b1a6bd3ab..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt +++ /dev/null @@ -1,28 +0,0 @@ -package app.omnivore.omnivore.persistence - -import androidx.room.Database -import androidx.room.RoomDatabase -import app.omnivore.omnivore.persistence.entities.* - -@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 -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt deleted file mode 100644 index 4d5f4cae6..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ /dev/null @@ -1,254 +0,0 @@ -package app.omnivore.omnivore.persistence.entities - -import android.util.Log -import androidx.core.net.toUri -import androidx.lifecycle.LiveData -import androidx.room.* -import app.omnivore.omnivore.models.ServerSyncStatus -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 - - if (savedItemId != other.savedItemId) return false - - return true - } - - 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) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertLabelCrossRefs(items: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertLabels(items: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertHighlights(items: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertHighlightCrossRefs(items: List) - - @Transaction - open fun insertAll(savedItems: List) { - insertSavedItems(savedItems.map { it.savedItem }) - - val labels: MutableList = mutableListOf() - val highlights: MutableList = mutableListOf() - - val labelCrossRefs: MutableList = mutableListOf() - val highlightCrossRefs: MutableList = 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 - - @Query("SELECT * FROM savedItem WHERE savedItemId = :itemID") - fun findById(itemID: String): SavedItem? - - @Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0") - fun getUnSynced(): List - - @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) - - @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 - - @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, sortKey: String, hasRequiredLabels: Int, hasExcludedLabels: Int, requiredLabels: List, excludedLabels: List, allowedContentReaders: List): LiveData> - - fun filteredLibraryData(allowedArchiveStates: List, sortKey: String, requiredLabels: List, excludedLabels: List, allowedContentReaders: List): LiveData> { - val result = _filteredLibraryData( - allowedArchiveStates = allowedArchiveStates, - sortKey = sortKey, - hasRequiredLabels = requiredLabels.size, - hasExcludedLabels = excludedLabels.size, - requiredLabels = requiredLabels, - excludedLabels = excludedLabels, - allowedContentReaders = allowedContentReaders - ) - return result - } -} - - - - -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 " -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt deleted file mode 100644 index cd17e939a..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt +++ /dev/null @@ -1,276 +0,0 @@ -package app.omnivore.omnivore.ui.library - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState -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.persistence.entities.SavedItemWithLabelsAndHighlights - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LibraryNavigationBar( - savedItemViewModel: SavedItemViewModel, - onSearchClicked: () -> Unit, - onAddLinkClicked: () -> Unit, - onSettingsIconClick: () -> Unit -) { - val actionsMenuItem: SavedItemWithLabelsAndHighlights? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) - - 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.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 = onSettingsIconClick) { -// Icon( -// imageVector = Icons.Default.MoreVert, -// contentDescription = null -// ) -// } - } ?: 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 - ) - } - } - } - ) -} -// -//@OptIn(ExperimentalMaterial3Api::class) -//@Composable -//fun LibraryBottomBar( -// savedItemViewModel: SavedItemViewModel, -// onSearchClicked: () -> Unit, -// onSettingsIconClick: () -> Unit -//) { -// val actionsMenuItem: SavedItemCardData? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) -// -// BottomAppBar( -//// colors = TopAppBarDefaults.topAppBarColors( -//// containerColor = if (actionsMenuItem == null) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.surfaceVariant -//// ), -//// trailingIcon = { -//// if (actionsMenuItem != null) { -//// IconButton(onClick = { -//// savedItemViewModel.actionsMenuItemLiveData.postValue(null) -//// }) { -//// Icon( -//// imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, -//// modifier = Modifier, -//// contentDescription = "Back" -//// ) -//// } -//// } -//// }, -// floatingActionButton = { -// IconButton(onClick = onSearchClicked) { -// Icon( -// imageVector = Icons.Outlined.Close, -// contentDescription = null -// ) -// } -// }, -// actions = { -// actionsMenuItem?.let { -// IconButton(onClick = onSearchClicked) { -// if (it.isArchived) { -// Icon( -// painter = painterResource(id = R.drawable.unarchive), -// contentDescription = null -// ) -// } else { -// Icon( -// painter = painterResource(id = R.drawable.archive_outline), -// contentDescription = null -// ) -// } -// } -// IconButton(onClick = onSearchClicked) { -// Icon( -// painter = painterResource(id = R.drawable.tag), -// contentDescription = null -// ) -// } -// IconButton(onClick = onSearchClicked) { -// Icon( -// imageVector = Icons.Outlined.Delete, -// contentDescription = null -// ) -// } -// IconButton(onClick = onSettingsIconClick) { -// Icon( -// imageVector = Icons.Default.MoreVert, -// contentDescription = null -// ) -// } -// } ?: run { -// IconButton(onClick = onSearchClicked) { -// Icon( -// imageVector = Icons.Filled.Search, -// contentDescription = null -// ) -// } -// -// IconButton(onClick = onSettingsIconClick) { -// Icon( -// imageVector = Icons.Default.MoreVert, -// contentDescription = null -// ) -// } -// } -// } -// ) -//} - - - -@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.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() - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemSortFilter.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemSortFilter.kt deleted file mode 100644 index 58f24174b..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemSortFilter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.omnivore.omnivore.ui.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() - } - ) - } - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt deleted file mode 100644 index a8b609253..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt +++ /dev/null @@ -1,199 +0,0 @@ -package app.omnivore.omnivore.ui.library - -import android.content.Intent -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import app.omnivore.omnivore.R -import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights -import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity -import app.omnivore.omnivore.persistence.entities.TypeaheadCardData -import app.omnivore.omnivore.ui.reader.PDFReaderActivity -import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard -import app.omnivore.omnivore.ui.savedItemViews.TypeaheadSearchCard - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SearchView( - viewModel: SearchViewModel, - navController: NavHostController -) { - val isRefreshing: Boolean by viewModel.isRefreshing.observeAsState(false) - val typeaheadMode: Boolean by viewModel.typeaheadMode.observeAsState(true) - val searchText: String by viewModel.searchTextLiveData.observeAsState("") - val actionsMenuItem: SavedItemWithLabelsAndHighlights? by viewModel.actionsMenuItemLiveData.observeAsState(null) - - Scaffold( - topBar = { - TopAppBar( - title = { Text("") }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ), - navigationIcon = { - if (actionsMenuItem != null) { - IconButton(onClick = { - viewModel.actionsMenuItemLiveData.postValue(null) - }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - modifier = Modifier, - contentDescription = "Back" - ) - } - } - }, - actions = { - if (actionsMenuItem != null) { - IconButton(onClick = { }) { - Icon( - painter = painterResource(id = R.drawable.archive_outline), - contentDescription = null - ) - } - IconButton(onClick = { }) { - Icon( - painter = painterResource(id = R.drawable.tag), - contentDescription = null - ) - } - IconButton(onClick = { }) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = null - ) - } - IconButton(onClick = { }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null - ) - } - } else { - Row { - SearchField( - searchText, - onSearch = { - viewModel.performSearch() - }, - onSearchTextChanged = { viewModel.updateSearchText(it) }, - navController = navController - ) - } - } - } - ) - - } - ) { paddingValues -> - if (isRefreshing) { - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .padding(top = paddingValues.calculateTopPadding()) - .fillMaxWidth() - ) { - CircularProgressIndicator( - modifier = Modifier - .height(45.dp) - .width(45.dp), - strokeWidth = 5.dp, - color = colorResource(R.color.green_55B938) - ) - } - } else if (typeaheadMode) { - TypeaheadSearchViewContent( - viewModel, - modifier = Modifier - .padding(top = paddingValues.calculateTopPadding()) - ) - } else { - SearchViewContent( - viewModel, - modifier = Modifier - .padding(top = paddingValues.calculateTopPadding()) - ) - } - } -} - -@Composable -fun TypeaheadSearchViewContent(viewModel: SearchViewModel, modifier: Modifier) { - val context = LocalContext.current - val listState = rememberLazyListState() - - val searchedCardsData: List 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 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) } - ) - } - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt deleted file mode 100644 index 4dd26c7dd..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ /dev/null @@ -1,362 +0,0 @@ -package app.omnivore.omnivore.ui.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 app.omnivore.omnivore.ui.reader.OmnivoreWebView.Direction -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) - -// val showHighlightColorPalette = webReaderViewModel.showHighlightColorPalette.observeAsState() -// val highlightColor = webReaderViewModel.highlightColor.observeAsState() - - 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(Direction.UP) - return@setOnKeyListener true - } - - KeyEvent.KEYCODE_VOLUME_DOWN -> { - scrollVertically(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() - } - }) -// if (showHighlightColorPalette.value == true) { -// HighlightColorPalette( -// mode = if (isDarkMode) HighlightColorPaletteMode.Dark else HighlightColorPaletteMode.Light, -// selectedColorName = highlightColor.value?.name ?: "yellow", -// onColorSelected = { -// webReaderViewModel.setHighlightColor(it) -// }, -// modifier = Modifier -// .align(Alignment.BottomCenter) -// .padding(12.dp, 12.dp, 12.dp, 36.dp) -// ) -// } - } -} - -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?) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt deleted file mode 100644 index f3945e24c..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt +++ /dev/null @@ -1,473 +0,0 @@ -package app.omnivore.omnivore.ui.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.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.material.MaterialTheme -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Home -import androidx.compose.material3.* -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -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.persistence.entities.SavedItemLabel -import app.omnivore.omnivore.ui.components.LabelsSelectionSheetContent -import app.omnivore.omnivore.ui.notebook.NotebookView -import app.omnivore.omnivore.ui.notebook.NotebookViewModel -import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu -import app.omnivore.omnivore.ui.theme.OmnivoreTheme -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import kotlin.math.roundToInt -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import app.omnivore.omnivore.ui.editinfo.EditInfoSheetContent -import app.omnivore.omnivore.ui.components.LabelsViewModel -import app.omnivore.omnivore.ui.editinfo.EditInfoViewModel -import app.omnivore.omnivore.ui.notebook.EditNoteModal - -@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(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 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 ?: 0xFF000000) - } - } ?: 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.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() - } - } - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt deleted file mode 100644 index a79915971..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ /dev/null @@ -1,565 +0,0 @@ -package app.omnivore.omnivore.ui.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.* -import app.omnivore.omnivore.DatastoreKeys -import app.omnivore.omnivore.DatastoreRepository -import app.omnivore.omnivore.EventTracker -import app.omnivore.omnivore.R -import app.omnivore.omnivore.dataService.* -import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput -import app.omnivore.omnivore.networking.* -import app.omnivore.omnivore.persistence.entities.SavedItem -import app.omnivore.omnivore.persistence.entities.SavedItemLabel -import app.omnivore.omnivore.ui.components.HighlightColor -import app.omnivore.omnivore.ui.library.SavedItemAction -import app.omnivore.omnivore.ui.setSavedItemLabels -import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull -import com.google.gson.Gson -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.distinctUntilChanged -import java.util.* -import javax.inject.Inject - - -data class WebReaderParams( - val item: SavedItem, - val articleContent: ArticleContent, - val labels: List -) - -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 = mutableListOf() - var maxToolbarHeightPx = 0.0f - - val webReaderParamsLiveData = MutableLiveData(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) - } - } - } - - 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 = 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) { - 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)) - } - } - } - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt deleted file mode 100644 index 35f90976f..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ /dev/null @@ -1,308 +0,0 @@ -package app.omnivore.omnivore.ui.savedItemViews - -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.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.* -import app.omnivore.omnivore.R -import app.omnivore.omnivore.persistence.entities.SavedItemLabel -import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights -import app.omnivore.omnivore.ui.components.LabelChip -import app.omnivore.omnivore.ui.components.LabelChipColors -import app.omnivore.omnivore.ui.library.SavedItemAction -import app.omnivore.omnivore.ui.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) { - // Log.d("selected", "is selected: ${selected}") - - 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( - 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 "" -} - -// -//var readingSpeed: Int64 { -// var result = UserDefaults.standard.integer(forKey: UserDefaultKey.userWordsPerMinute.rawValue) -// if result <= 0 { -// result = 235 -// } -// return Int64(result) -//} - -fun estimatedReadingTime(item: SavedItemWithLabelsAndHighlights): String { - item.savedItem.wordsCount?.let { - if (it > 0) { - val readLen = 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 - ) - -// Text("\(highlightsText)") -// -// Text("\(notesText)") - - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemContextMenu.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemContextMenu.kt deleted file mode 100644 index 242a2a88e..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemContextMenu.kt +++ /dev/null @@ -1,101 +0,0 @@ -package app.omnivore.omnivore.ui.savedItemViews - -import android.content.Context -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.Share -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import app.omnivore.omnivore.R -import app.omnivore.omnivore.ui.library.SavedItemAction -import app.omnivore.omnivore.ui.reader.WebReaderViewModel - -@Composable -fun SavedItemContextMenu( - isExpanded: Boolean, - isArchived: Boolean, - context: Context, - webReaderViewModel: WebReaderViewModel, - onDismiss: () -> Unit, - actionHandler: (SavedItemAction) -> Unit -) { - DropdownMenu( - expanded = isExpanded, - onDismissRequest = onDismiss - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.saved_item_context_menu_action_edit_info)) }, - onClick = { - actionHandler(SavedItemAction.EditInfo) - onDismiss() - }, - leadingIcon = { - Icon( - Icons.Outlined.Info, - contentDescription = null - ) - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.saved_item_context_menu_action_edit_labels)) }, - onClick = { - actionHandler(SavedItemAction.EditLabels) - onDismiss() - }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.tag), - contentDescription = null - ) - } - ) - DropdownMenuItem( - text = { Text(if (isArchived) - stringResource(R.string.saved_item_context_menu_action_unarchive) else - stringResource(R.string.saved_item_context_menu_action_archive)) }, - onClick = { - val action = if (isArchived) SavedItemAction.Unarchive else SavedItemAction.Archive - actionHandler(action) - onDismiss() - }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.archive_outline), - contentDescription = null - ) - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.saved_item_context_menu_action_share_original)) }, - onClick = { - webReaderViewModel.showShareLinkSheet(context) - onDismiss() - }, - leadingIcon = { - Icon( - Icons.Outlined.Share, - contentDescription = null - ) - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.saved_item_context_menu_action_remove_item)) }, - onClick = { - actionHandler(SavedItemAction.Delete) - onDismiss() - }, - leadingIcon = { - Icon( - Icons.Outlined.Delete, - contentDescription = null - ) - } - ) - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt deleted file mode 100644 index 9d73fe181..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt +++ /dev/null @@ -1,67 +0,0 @@ -package app.omnivore.omnivore.ui.settings - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -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) - } - } - } -} - diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/utils/Constants.kt similarity index 94% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/utils/Constants.kt index 27b2829f9..b4962979a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/utils/Constants.kt @@ -1,4 +1,6 @@ -package app.omnivore.omnivore +package app.omnivore.omnivore.utils + +import app.omnivore.omnivore.BuildConfig object Constants { const val apiURL = BuildConfig.OMNIVORE_API_URL diff --git a/android/Omnivore/app/src/main/res/values/strings.xml b/android/Omnivore/app/src/main/res/values/strings.xml index ad478efa8..d1cce247c 100644 --- a/android/Omnivore/app/src/main/res/values/strings.xml +++ b/android/Omnivore/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ Note Copy Note + Back Continue with Apple @@ -186,6 +187,8 @@ Unarchive Share Original Remove Item + Mark read + Mark unread Logout diff --git a/android/Omnivore/build.gradle b/android/Omnivore/build.gradle index 62770128d..eb3032ec3 100644 --- a/android/Omnivore/build.gradle +++ b/android/Omnivore/build.gradle @@ -1,10 +1,9 @@ buildscript { ext { - compose_version = '1.6.0' - lifecycle_version = '2.5.1' - hilt_version = '2.44.2' - gradle_plugin_version = '7.4.2' - room_version = '2.4.3' + lifecycle_version = "2.7.0" + hilt_version = "2.50" + gradle_plugin_version = '8.2.2' + room_version = "2.6.1" kotlin_version = '1.7.10' } @@ -21,7 +20,7 @@ buildscript { plugins { id 'com.android.application' version "$gradle_plugin_version" apply false id 'com.android.library' version "$gradle_plugin_version" apply false - id 'org.jetbrains.kotlin.android' version '1.7.10' apply false + id "org.jetbrains.kotlin.android" version "1.9.22" apply false } task clean(type: Delete) { diff --git a/android/Omnivore/gradle.properties b/android/Omnivore/gradle.properties index b5987b651..dd3e782bb 100644 --- a/android/Omnivore/gradle.properties +++ b/android/Omnivore/gradle.properties @@ -22,3 +22,4 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true +android.nonFinalResIds=false diff --git a/android/Omnivore/gradle/wrapper/gradle-wrapper.properties b/android/Omnivore/gradle/wrapper/gradle-wrapper.properties index 5bf8d8081..58a908ba3 100644 --- a/android/Omnivore/gradle/wrapper/gradle-wrapper.properties +++ b/android/Omnivore/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Aug 10 17:39:47 PDT 2022 +#Thu Feb 01 13:22:21 GMT 2024 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/Omnivore/settings.gradle b/android/Omnivore/settings.gradle.kts similarity index 87% rename from android/Omnivore/settings.gradle rename to android/Omnivore/settings.gradle.kts index d3111f0f7..038c37816 100644 --- a/android/Omnivore/settings.gradle +++ b/android/Omnivore/settings.gradle.kts @@ -13,8 +13,8 @@ dependencyResolutionManagement { maven { url = uri("https://customers.pspdfkit.com/maven") } - maven { url 'https://jitpack.io' } + maven(url = "https://jitpack.io") } } rootProject.name = "Omnivore" -include ':app' +include(":app")