diff --git a/app/src/main/java/org/yrovas/linklater/AppComponent.kt b/app/src/main/java/org/yrovas/linklater/AppComponent.kt index 17d9ab3..c1bddfa 100644 --- a/app/src/main/java/org/yrovas/linklater/AppComponent.kt +++ b/app/src/main/java/org/yrovas/linklater/AppComponent.kt @@ -23,19 +23,25 @@ import kotlinx.serialization.json.Json import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides import me.tatarka.inject.annotations.Scope -import org.yrovas.linklater.data.local.BookmarkDataSource import org.yrovas.linklater.data.local.BookmarkDataSourceImpl import org.yrovas.linklater.data.local.PrefDataStore import org.yrovas.linklater.data.local.PrefStore -import org.yrovas.linklater.data.local.TagDataSource import org.yrovas.linklater.data.local.TagDataSourceImpl -import org.yrovas.linklater.data.remote.BookmarkAPI import org.yrovas.linklater.data.remote.LinkDingAPI -import org.yrovas.linklater.ui.activity.DestinationHost +import org.yrovas.linklater.domain.BookmarkAPI +import org.yrovas.linklater.domain.BookmarkDataSource +import org.yrovas.linklater.domain.TagDataSource +import org.yrovas.linklater.ui.common.DestinationHost +const val TAG = "DEBUG/create" val Context.dataStore: DataStore by preferencesDataStore(name = "preferences") @Scope +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER +) annotation class AppScope @Component @@ -47,14 +53,19 @@ abstract class AppComponent( abstract val prefStore: PrefDataStore val store: DataStore - @Provides get() = context.dataStore + @AppScope @Provides get() = context.dataStore + @AppScope @Provides - fun providePrefStore(store: DataStore): PrefDataStore = - PrefStore(store) + fun providePrefStore(store: DataStore): PrefDataStore { + Log.d(TAG, "providePrefStore: CREATE") + return PrefStore(store) + } + @AppScope @Provides fun provideHttpClient(): HttpClient = HttpClient(Android) { + Log.d(TAG, "provideHttpClient: CREATE") install(Logging) { logger = object : Logger { override fun log(message: String) { @@ -77,20 +88,26 @@ abstract class AppComponent( abstract val linkDingAPI: LinkDingAPI val bookmarkAPI: BookmarkAPI + @AppScope @Provides get() = linkDingAPI abstract val bookmarkDataSource: BookmarkDataSource + @AppScope @Provides fun provideBookmarkDataSource(db: Database): BookmarkDataSource = BookmarkDataSourceImpl(db) + @AppScope @Provides fun provideTagDataSource(db: Database): TagDataSource = TagDataSourceImpl(db) + @AppScope @Provides - fun provideSQLDriver(context: Context): SqlDriver = - AndroidSqliteDriver(schema = Database.Schema, + fun provideSQLDriver(context: Context): SqlDriver { + Log.d(TAG, "provideSQLDriver: CREATE") + return AndroidSqliteDriver( + schema = Database.Schema, context = context, name = "linklater.db", callback = object : AndroidSqliteDriver.Callback(Database.Schema) { @@ -98,7 +115,12 @@ abstract class AppComponent( db.setForeignKeyConstraintsEnabled(true) } }) + } + @AppScope @Provides - fun provideDB(driver: SqlDriver): Database = Database(driver) + fun provideDB(driver: SqlDriver): Database { + Log.d(TAG, "provideDB: CREATE") + return Database(driver) + } } diff --git a/app/src/main/java/org/yrovas/linklater/data/local/BookmarkDataSourceImpl.kt b/app/src/main/java/org/yrovas/linklater/data/local/BookmarkDataSourceImpl.kt index 496fd93..af84ebd 100644 --- a/app/src/main/java/org/yrovas/linklater/data/local/BookmarkDataSourceImpl.kt +++ b/app/src/main/java/org/yrovas/linklater/data/local/BookmarkDataSourceImpl.kt @@ -3,6 +3,7 @@ package org.yrovas.linklater.data.local import android.util.Log import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList +import app.cash.sqldelight.coroutines.mapToOne import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -10,10 +11,15 @@ import kotlinx.coroutines.withContext import org.yrovas.linklater.Database import org.yrovas.linklater.data.Bookmark import org.yrovas.linklater.data.toBookmark +import org.yrovas.linklater.domain.BookmarkDataSource +import kotlin.math.log const val TAG = "DEBUG" class BookmarkDataSourceImpl(db: Database) : BookmarkDataSource { + init { + Log.d("DEBUG/create", "BookmarkDataSourceImpl: CREATE") + } private val q = db.bookmarkTagsQueries override suspend fun getBookmark(id: Long): Bookmark? { @@ -29,9 +35,36 @@ class BookmarkDataSourceImpl(db: Database) : BookmarkDataSource { } } + override fun getBookmarkCount(): Int { + return q.countBookmarks().executeAsOneOrNull()?.toInt() ?: -1 +// .map { Log.d(TAG, "getBookmarkCount: EMIT $it"); it.toInt() } + } + override suspend fun insertBookmarks(bookmarks: List) { - bookmarks.forEach { - insertBookmark(it) + withContext(Dispatchers.IO) { + q.transaction { + bookmarks.forEach { bookmark -> + val id = q.insertBookmark( + id = bookmark.id, + url = bookmark.url, + title = bookmark.title, + description = bookmark.description, + notes = bookmark.notes, + website_title = bookmark.website_title, + website_description = bookmark.website_description, + is_archived = bookmark.is_archived, + unread = bookmark.unread, + shared = bookmark.shared, + date_added = bookmark.date_added, + date_modified = bookmark.date_modified + ).executeAsOneOrNull()!! + bookmark.tags.forEach { + q.insertTag(name = it) + val tagID = q.getTagByName(it).executeAsOneOrNull()!! + q.insertTagForBookmark(bookmarkID = id, tagID = tagID) + } + } + } } } @@ -62,10 +95,7 @@ class BookmarkDataSourceImpl(db: Database) : BookmarkDataSource { q.insertTag(name = it) val tagID = q.getTagByName(it).executeAsOneOrNull()!! Log.d(TAG, "inserted TAG: $it,$tagID") - q.insertTagForBookmark( - bookmarkID = id, - tagID = tagID, - ) + q.insertTagForBookmark(bookmarkID = id, tagID = tagID) } } } diff --git a/app/src/main/java/org/yrovas/linklater/data/local/EmptyBookmarkSource.kt b/app/src/main/java/org/yrovas/linklater/data/local/EmptyBookmarkSource.kt index ea2e80e..e10b7e7 100644 --- a/app/src/main/java/org/yrovas/linklater/data/local/EmptyBookmarkSource.kt +++ b/app/src/main/java/org/yrovas/linklater/data/local/EmptyBookmarkSource.kt @@ -2,7 +2,9 @@ package org.yrovas.linklater.data.local import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow import org.yrovas.linklater.data.Bookmark +import org.yrovas.linklater.domain.BookmarkDataSource class EmptyBookmarkSource : BookmarkDataSource { override suspend fun getBookmark(id: Long): Bookmark? { @@ -13,6 +15,10 @@ class EmptyBookmarkSource : BookmarkDataSource { return emptyList>().asFlow() } + override fun getBookmarkCount(): Int { + return 0 + } + override suspend fun insertBookmark(bookmark: Bookmark) {} override suspend fun insertBookmarks(bookmarks: List) {} override suspend fun deleteBookmark(id: Long) {} diff --git a/app/src/main/java/org/yrovas/linklater/data/local/PrefDataStore.kt b/app/src/main/java/org/yrovas/linklater/data/local/PrefDataStore.kt index 329ba3b..4394ad4 100644 --- a/app/src/main/java/org/yrovas/linklater/data/local/PrefDataStore.kt +++ b/app/src/main/java/org/yrovas/linklater/data/local/PrefDataStore.kt @@ -2,7 +2,9 @@ package org.yrovas.linklater.data.local import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.flow.Flow +import org.yrovas.linklater.AppScope +@AppScope interface PrefDataStore { suspend fun getPrefs(key: Preferences.Key, default: T): Flow suspend fun getPref(key: Preferences.Key, default: T): T diff --git a/app/src/main/java/org/yrovas/linklater/data/local/TagDataSourceImpl.kt b/app/src/main/java/org/yrovas/linklater/data/local/TagDataSourceImpl.kt index 4a481cd..b3e7a4e 100644 --- a/app/src/main/java/org/yrovas/linklater/data/local/TagDataSourceImpl.kt +++ b/app/src/main/java/org/yrovas/linklater/data/local/TagDataSourceImpl.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.yrovas.linklater.Database +import org.yrovas.linklater.domain.TagDataSource class TagDataSourceImpl(db: Database) : TagDataSource { private val q = db.bookmarkTagsQueries diff --git a/app/src/main/java/org/yrovas/linklater/data/remote/EmptyBookmarkAPI.kt b/app/src/main/java/org/yrovas/linklater/data/remote/EmptyBookmarkAPI.kt index 462b595..6637ab1 100644 --- a/app/src/main/java/org/yrovas/linklater/data/remote/EmptyBookmarkAPI.kt +++ b/app/src/main/java/org/yrovas/linklater/data/remote/EmptyBookmarkAPI.kt @@ -1,23 +1,31 @@ package org.yrovas.linklater.data.remote -import android.content.Context import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.yrovas.linklater.data.Bookmark import org.yrovas.linklater.data.LocalBookmark import org.yrovas.linklater.domain.APIError +import org.yrovas.linklater.domain.BookmarkAPI import org.yrovas.linklater.domain.Err import org.yrovas.linklater.domain.Ok import org.yrovas.linklater.domain.Res class EmptyBookmarkAPI : BookmarkAPI { - override suspend fun authenticate( + override fun authenticate( endpoint: String?, token: String?, - validate: Boolean, ): Res { return Ok(Unit) } + override suspend fun checkConnection(): Res { + return Ok(Unit) + } + + override val authProvided: StateFlow + get() = MutableStateFlow(false) + override suspend fun getBookmarks( page: Int, query: String?, @@ -28,7 +36,7 @@ class EmptyBookmarkAPI : BookmarkAPI { } override suspend fun saveBookmark(bookmark: LocalBookmark): Res { - return Err(APIError.CONNECTION) + return Err(APIError.NO_CONNECTION) } override suspend fun getTags(page: Int): Res, APIError> { diff --git a/app/src/main/java/org/yrovas/linklater/data/remote/LinkDingAPI.kt b/app/src/main/java/org/yrovas/linklater/data/remote/LinkDingAPI.kt index 6b3b893..9ce1f4b 100644 --- a/app/src/main/java/org/yrovas/linklater/data/remote/LinkDingAPI.kt +++ b/app/src/main/java/org/yrovas/linklater/data/remote/LinkDingAPI.kt @@ -1,6 +1,5 @@ package org.yrovas.linklater.data.remote -import android.content.Context import android.util.Log import io.ktor.client.HttpClient import io.ktor.client.call.body @@ -8,10 +7,11 @@ import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.client.request.post import io.ktor.client.request.setBody -import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.encodeToJsonElement import me.tatarka.inject.annotations.Inject import org.yrovas.linklater.AppScope import org.yrovas.linklater.checkBookmarkAPIToken @@ -19,11 +19,10 @@ import org.yrovas.linklater.checkURL import org.yrovas.linklater.data.Bookmark import org.yrovas.linklater.data.LocalBookmark import org.yrovas.linklater.domain.APIError +import org.yrovas.linklater.domain.BookmarkAPI import org.yrovas.linklater.domain.Err import org.yrovas.linklater.domain.Ok import org.yrovas.linklater.domain.Res -import org.yrovas.linklater.domain.toRes -import java.io.File @AppScope @Inject @@ -34,30 +33,33 @@ class LinkDingAPI( private val pageSize: Int = 20, ) : BookmarkAPI { init { - Log.d("DEBUG", "CREATING BOOKMARK API: ") + Log.d("DEBUG/create", "LinkDingAPI: CREATE") } - private val authProvided - get() = !endpoint.isNullOrBlank() && !token.isNullOrBlank() + private val _authProvided = MutableStateFlow(false) + override val authProvided: StateFlow = _authProvided.asStateFlow() - override suspend fun authenticate( + override fun authenticate( endpoint: String?, token: String?, - validate: Boolean, ): Res { Log.d("DEBUG", "authenticate: with endpoint: $endpoint") if (!endpoint.isNullOrBlank()) { - if (!checkURL(endpoint)) return Err(APIError.AUTH) + if (!checkURL(endpoint)) return Err(APIError.INCORRECT_ENDPOINT) this.endpoint = endpoint } if (!token.isNullOrBlank()) { - if (!checkBookmarkAPIToken(token)) return Err(APIError.AUTH) + if (!checkBookmarkAPIToken(token)) return Err(APIError.INCORRECT_AUTH) this.token = token } - if (!validate) return Ok(Unit) + _authProvided.update { !endpoint.isNullOrBlank() && !token.isNullOrBlank() } + if (!authProvided.value) return Err(APIError.INCORRECT_AUTH) + return Ok(Unit) + } - if (!authProvided) return Err(APIError.AUTH) + override suspend fun checkConnection(): Res { + if (!authProvided.value) return Err(APIError.INCORRECT_AUTH) return when (val res = getBookmarks(page = 0)) { is Res.Err -> Err(res.error) @@ -70,8 +72,8 @@ class LinkDingAPI( query: String?, ): Res, APIError> { // delay(3200) - if (!authProvided) return Err(APIError.AUTH) - return runCatching { + if (!authProvided.value) return Err(APIError.INCORRECT_AUTH) + return try { Log.d("DEBUG/net", "getBookmarks: starting request") val response = client.get("${endpoint!!}/bookmarks/") { header("Authorization", "Token ${token!!}") @@ -84,16 +86,15 @@ class LinkDingAPI( url.parameters.append("q", query) } } -// Json.decodeFromString(response.bodyAsText()).results - response.body().results // ?? very kool Ktor - }.onFailure { - Log.i("DEBUG/net", "getBookmarks: ${it.message}") - Result.failure>(it) - }.toRes(withError = APIError.CONNECTION) + Ok(response.body().results) + } catch (e: Exception) { + Log.i("DEBUG/net", "getBookmarks: ${e.message}") + Err(APIError.NO_CONNECTION) + } } override suspend fun saveBookmark(bookmark: LocalBookmark): Res { - if (!authProvided) return Err(APIError.AUTH) + if (!authProvided.value) return Err(APIError.INCORRECT_AUTH) return try { val response = client.post("${endpoint!!}/bookmarks/") { setBody(bookmark) @@ -101,16 +102,16 @@ class LinkDingAPI( } when (response.status.value) { in 200..299 -> Ok(response.body()) - else -> Err(APIError.AUTH) + else -> Err(APIError.INCORRECT_AUTH) } } catch (e: Exception) { - Err(APIError.CONNECTION) + Err(APIError.NO_CONNECTION) } } override suspend fun getTags(page: Int): Res, APIError> { - if (!authProvided) return Err(APIError.AUTH) - return runCatching { + if (!authProvided.value) return Err(APIError.INCORRECT_AUTH) + return try { Log.d("DEBUG/net", "getTags: starting request") val response = client.get("${endpoint!!}/tags/") { header("Authorization", "Token ${token!!}") @@ -118,11 +119,10 @@ class LinkDingAPI( url.parameters.append("offset", (pageSize * page).toString()) } } - response.body().results - }.onFailure { - Log.i("DEBUG/net", "getTags: ${it.message}") - Result.failure>(it) - }.toRes(APIError.CONNECTION) + Ok(response.body().results) + } catch (e: Exception) { + Err(APIError.NO_CONNECTION) + } } // @Serializable diff --git a/app/src/main/java/org/yrovas/linklater/domain/APIError.kt b/app/src/main/java/org/yrovas/linklater/domain/APIError.kt index c7750cf..055ae08 100644 --- a/app/src/main/java/org/yrovas/linklater/domain/APIError.kt +++ b/app/src/main/java/org/yrovas/linklater/domain/APIError.kt @@ -1,3 +1,8 @@ package org.yrovas.linklater.domain -enum class APIError : Error { CONNECTION, AUTH } +enum class APIError : Error { + NO_CONNECTION, + INCORRECT_ENDPOINT, + INCORRECT_AUTH, + NO_AUTH_PROVIDED +} diff --git a/app/src/main/java/org/yrovas/linklater/data/remote/BookmarkAPI.kt b/app/src/main/java/org/yrovas/linklater/domain/BookmarkAPI.kt similarity index 62% rename from app/src/main/java/org/yrovas/linklater/data/remote/BookmarkAPI.kt rename to app/src/main/java/org/yrovas/linklater/domain/BookmarkAPI.kt index 77046e1..8b932da 100644 --- a/app/src/main/java/org/yrovas/linklater/data/remote/BookmarkAPI.kt +++ b/app/src/main/java/org/yrovas/linklater/domain/BookmarkAPI.kt @@ -1,13 +1,15 @@ -package org.yrovas.linklater.data.remote +package org.yrovas.linklater.domain -import android.content.Context +import kotlinx.coroutines.flow.StateFlow import org.yrovas.linklater.data.Bookmark import org.yrovas.linklater.data.LocalBookmark import org.yrovas.linklater.domain.APIError import org.yrovas.linklater.domain.Res interface BookmarkAPI { - suspend fun authenticate(endpoint: String? = null, token: String? = null, validate: Boolean = false): Res + val authProvided: StateFlow + fun authenticate(endpoint: String? = null, token: String? = null): Res + suspend fun checkConnection(): Res suspend fun getBookmarks(page: Int, query: String? = null): Res, APIError> suspend fun saveBookmark(bookmark: LocalBookmark): Res suspend fun getTags(page: Int): Res, APIError> diff --git a/app/src/main/java/org/yrovas/linklater/data/local/BookmarkDataSource.kt b/app/src/main/java/org/yrovas/linklater/domain/BookmarkDataSource.kt similarity index 84% rename from app/src/main/java/org/yrovas/linklater/data/local/BookmarkDataSource.kt rename to app/src/main/java/org/yrovas/linklater/domain/BookmarkDataSource.kt index 9f8dd94..0f33219 100644 --- a/app/src/main/java/org/yrovas/linklater/data/local/BookmarkDataSource.kt +++ b/app/src/main/java/org/yrovas/linklater/domain/BookmarkDataSource.kt @@ -1,4 +1,4 @@ -package org.yrovas.linklater.data.local +package org.yrovas.linklater.domain import kotlinx.coroutines.flow.Flow import org.yrovas.linklater.data.Bookmark @@ -6,6 +6,7 @@ import org.yrovas.linklater.data.Bookmark interface BookmarkDataSource { suspend fun getBookmark(id: Long): Bookmark? fun getBookmarks(): Flow> + fun getBookmarkCount(): Int suspend fun insertBookmark(bookmark: Bookmark) suspend fun insertBookmarks(bookmarks: List) suspend fun deleteBookmark(id: Long) diff --git a/app/src/main/java/org/yrovas/linklater/domain/Res.kt b/app/src/main/java/org/yrovas/linklater/domain/Res.kt index 6765124..2c03946 100644 --- a/app/src/main/java/org/yrovas/linklater/domain/Res.kt +++ b/app/src/main/java/org/yrovas/linklater/domain/Res.kt @@ -17,6 +17,7 @@ val Res.isOk: Boolean is Res.Ok -> true is Res.Err -> false } + val Res.isErr: Boolean get() = !isOk @@ -55,6 +56,20 @@ fun Res.then(ok: (data: D) -> T, err: (error: E) - } } +fun Res.into(withData: D): Res { + return when (this) { + is Res.Err -> Res.Err(this.error) + is Res.Ok -> Res.Ok(withData) + } +} + +fun Res.into(withError: E): Res { + return when (this) { + is Res.Err -> Err(withError) + is Res.Ok -> Ok(this.data) + } +} + fun Res?.then( ok: (data: D) -> T, err: (error: E) -> T, @@ -83,6 +98,21 @@ fun Res?.apply( return this?.apply(ok, err) ?: nil() } +fun Res.errorOrNull(): E? { + return when (this) { + is Res.Err -> error + is Res.Ok -> null + } +} + +fun Res.errorOrThrow(): E { + return (this as Res.Err).error +} + +fun Res.getOrThrow(): D { + return (this as Res.Ok).data +} + fun Res.getOrNull(): D? { return when (this) { is Res.Err -> null diff --git a/app/src/main/java/org/yrovas/linklater/data/local/TagDataSource.kt b/app/src/main/java/org/yrovas/linklater/domain/TagDataSource.kt similarity index 88% rename from app/src/main/java/org/yrovas/linklater/data/local/TagDataSource.kt rename to app/src/main/java/org/yrovas/linklater/domain/TagDataSource.kt index 765d514..b45d92b 100644 --- a/app/src/main/java/org/yrovas/linklater/data/local/TagDataSource.kt +++ b/app/src/main/java/org/yrovas/linklater/domain/TagDataSource.kt @@ -1,4 +1,4 @@ -package org.yrovas.linklater.data.local +package org.yrovas.linklater.domain import kotlinx.coroutines.flow.Flow import org.yrovas.linklater.data.Bookmark diff --git a/app/src/main/java/org/yrovas/linklater/ui/activity/AppActivity.kt b/app/src/main/java/org/yrovas/linklater/ui/activity/AppActivity.kt index 39c0109..12f5901 100644 --- a/app/src/main/java/org/yrovas/linklater/ui/activity/AppActivity.kt +++ b/app/src/main/java/org/yrovas/linklater/ui/activity/AppActivity.kt @@ -1,51 +1,38 @@ package org.yrovas.linklater.ui.activity -import android.annotation.SuppressLint import android.content.Intent +import android.os.StrictMode +import android.os.StrictMode.VmPolicy +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope -import com.ramcosta.composedestinations.DestinationsNavHost -import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination -import com.ramcosta.composedestinations.generated.destinations.PreferencesScreenDestination -import com.ramcosta.composedestinations.generated.destinations.SaveBookmarkActivityScreenDestination -import com.ramcosta.composedestinations.generated.destinations.SaveBookmarkScreenDestination -import com.ramcosta.composedestinations.navigation.DependenciesContainerBuilder -import com.ramcosta.composedestinations.navigation.dependency -import com.ramcosta.composedestinations.navigation.destination -import com.ramcosta.composedestinations.spec.DestinationSpec import com.ramcosta.composedestinations.spec.NavHostGraphSpec -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import me.tatarka.inject.annotations.Inject import org.yrovas.linklater.AppComponent import org.yrovas.linklater.create import org.yrovas.linklater.data.local.Prefs -import org.yrovas.linklater.ui.state.HomeScreenState -import org.yrovas.linklater.ui.state.PreferencesScreenState -import org.yrovas.linklater.ui.state.SaveBookmarkScreenState import org.yrovas.linklater.ui.theme.AppTheme abstract class AppActivity : ComponentActivity() { - val component by lazy(LazyThreadSafetyMode.NONE) { + private val component by lazy(LazyThreadSafetyMode.NONE) { + Log.d("DEBUG/create", "AppComponent: CREATE") AppComponent::class.create(this) } - private val _setup_complete: MutableStateFlow = - MutableStateFlow(false) - private var setup_complete = _setup_complete.asStateFlow() fun launch(job: suspend () -> Unit) { lifecycleScope.launch { job() } } protected fun setContent(navGraph: NavHostGraphSpec) { + StrictMode.setVmPolicy( + VmPolicy.Builder(StrictMode.getVmPolicy()) + .detectLeakedClosableObjects() + .build() + ) + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); val destinationHost = component.destinationHost @@ -55,46 +42,12 @@ abstract class AppActivity : ComponentActivity() { endpoint = component.prefStore.getPref(Prefs.LINKDING_URL, ""), token = component.prefStore.getPref(Prefs.LINKDING_TOKEN, ""), ) - _setup_complete.update { true } } setContent { val snackState = remember { SnackbarHostState() } AppTheme { - destinationHost(navGraph, snackState, setup_complete) + destinationHost(navGraph, snackState) } } } } - -typealias DestinationHost = @Composable (NavHostGraphSpec, SnackbarHostState, StateFlow) -> Unit - -@Inject -@Composable -fun DestinationHost( - homeScreenState: () -> HomeScreenState, - preferencesScreenState: () -> PreferencesScreenState, - saveBookmarkScreenState: () -> SaveBookmarkScreenState, - navGraph: NavHostGraphSpec, - snackbarHostState: SnackbarHostState, - setup_complete: StateFlow, -) { - DestinationsNavHost(navGraph = navGraph, dependenciesContainerBuilder = { - dependency(snackbarHostState) - destination(HomeScreenDestination) { - dependency(setup_complete) - } - provideState(HomeScreenDestination, homeScreenState) - provideState(SaveBookmarkScreenDestination, saveBookmarkScreenState) - provideState(SaveBookmarkActivityScreenDestination, saveBookmarkScreenState) - provideState(PreferencesScreenDestination, preferencesScreenState) - }) -} - -@SuppressLint("ComposableNaming") -@Composable -private fun DependenciesContainerBuilder.provideState( - destination: DestinationSpec, - state: () -> ViewModel, -) { - destination(destination) { dependency(state) } -} diff --git a/app/src/main/java/org/yrovas/linklater/ui/common/BookmarkRow.kt b/app/src/main/java/org/yrovas/linklater/ui/common/BookmarkRow.kt index 2ef2988..8dd0fe6 100644 --- a/app/src/main/java/org/yrovas/linklater/ui/common/BookmarkRow.kt +++ b/app/src/main/java/org/yrovas/linklater/ui/common/BookmarkRow.kt @@ -23,13 +23,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.core.net.toUri +import io.ktor.http.Url import kotlinx.datetime.Clock import kotlinx.datetime.Instant import org.yrovas.linklater.data.Bookmark import org.yrovas.linklater.openUri import org.yrovas.linklater.timeAgo import org.yrovas.linklater.ui.theme.padding -import java.net.URI @Composable fun BookmarkRow( @@ -45,7 +45,7 @@ fun BookmarkRow( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = URI(bookmark.url).host ?: bookmark.url, + text = Url(bookmark.url).host, overflow = TextOverflow.Ellipsis, style = typography.labelLarge, color = colorScheme.outline, @@ -56,7 +56,7 @@ fun BookmarkRow( ), style = typography.labelLarge, color = colorScheme.outline ) } - Spacer(modifier = androidx.compose.ui.Modifier.height(padding.tiny)) + Spacer(modifier = Modifier.height(padding.tiny)) Row { Text(text = if (!bookmark.title.isNullOrBlank()) { bookmark.title diff --git a/app/src/main/java/org/yrovas/linklater/ui/common/DestinationHost.kt b/app/src/main/java/org/yrovas/linklater/ui/common/DestinationHost.kt new file mode 100644 index 0000000..e5a94a5 --- /dev/null +++ b/app/src/main/java/org/yrovas/linklater/ui/common/DestinationHost.kt @@ -0,0 +1,52 @@ +package org.yrovas.linklater.ui.common + +import android.annotation.SuppressLint +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.lifecycle.ViewModel +import com.ramcosta.composedestinations.DestinationsNavHost +import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination +import com.ramcosta.composedestinations.generated.destinations.PreferencesScreenDestination +import com.ramcosta.composedestinations.generated.destinations.SaveBookmarkActivityScreenDestination +import com.ramcosta.composedestinations.generated.destinations.SaveBookmarkScreenDestination +import com.ramcosta.composedestinations.navigation.DependenciesContainerBuilder +import com.ramcosta.composedestinations.navigation.dependency +import com.ramcosta.composedestinations.navigation.destination +import com.ramcosta.composedestinations.spec.DestinationSpec +import com.ramcosta.composedestinations.spec.NavHostGraphSpec +import me.tatarka.inject.annotations.Assisted +import me.tatarka.inject.annotations.Inject +import org.yrovas.linklater.ui.state.HomeScreenState +import org.yrovas.linklater.ui.state.PreferencesScreenState +import org.yrovas.linklater.ui.state.SaveBookmarkScreenState + +typealias DestinationHost = @Composable (NavHostGraphSpec, SnackbarHostState) -> Unit + +@Inject +@Composable +fun DestinationHost( + homeScreenState: () -> HomeScreenState, + preferencesScreenState: () -> PreferencesScreenState, + saveBookmarkScreenState: () -> SaveBookmarkScreenState, + @Assisted navGraph: NavHostGraphSpec, + @Assisted snackbarHostState: SnackbarHostState, +) { + DestinationsNavHost(navGraph = navGraph, dependenciesContainerBuilder = { + dependency(snackbarHostState) + provideState(HomeScreenDestination, homeScreenState) + provideState(SaveBookmarkScreenDestination, saveBookmarkScreenState) + provideState( + SaveBookmarkActivityScreenDestination, saveBookmarkScreenState + ) + provideState(PreferencesScreenDestination, preferencesScreenState) + }) +} + +@SuppressLint("ComposableNaming") +@Composable +private fun DependenciesContainerBuilder.provideState( + destination: DestinationSpec, + state: () -> ViewModel, +) { + destination(destination) { dependency(state) } +} diff --git a/app/src/main/java/org/yrovas/linklater/ui/screens/HomeScreen.kt b/app/src/main/java/org/yrovas/linklater/ui/screens/HomeScreen.kt index 25fcd39..500801f 100644 --- a/app/src/main/java/org/yrovas/linklater/ui/screens/HomeScreen.kt +++ b/app/src/main/java/org/yrovas/linklater/ui/screens/HomeScreen.kt @@ -1,9 +1,5 @@ package org.yrovas.linklater.ui.screens -import android.util.Log -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -11,17 +7,17 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddLink import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.ramcosta.composedestinations.annotation.Destination @@ -29,25 +25,16 @@ import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.PreferencesScreenDestination import com.ramcosta.composedestinations.generated.destinations.SaveBookmarkScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import org.yrovas.linklater.ThemePreview -import org.yrovas.linklater.data.Bookmark -import org.yrovas.linklater.data.local.EmptyBookmarkSource -import org.yrovas.linklater.data.remote.EmptyBookmarkAPI import org.yrovas.linklater.domain.APIError -import org.yrovas.linklater.domain.Err -import org.yrovas.linklater.domain.Ok import org.yrovas.linklater.ui.common.AppBar import org.yrovas.linklater.ui.common.BookmarkRow import org.yrovas.linklater.ui.common.Frame import org.yrovas.linklater.ui.common.Icon import org.yrovas.linklater.ui.common.RefreshIcon import org.yrovas.linklater.ui.state.HomeScreenState -import org.yrovas.linklater.ui.theme.AppTheme +import org.yrovas.linklater.ui.state.HomeScreenState.Effect +import org.yrovas.linklater.ui.state.HomeScreenState.Event import org.yrovas.linklater.ui.theme.padding @Destination(start = true) @@ -55,38 +42,33 @@ import org.yrovas.linklater.ui.theme.padding fun HomeScreen( nav: DestinationsNavigator, snackState: SnackbarHostState, - setup_complete: StateFlow, state: () -> HomeScreenState, ) { @Suppress("NAME_SHADOWING") val state = viewModel { state() } - @Suppress("NAME_SHADOWING") val setup_complete by setup_complete.collectAsState() val scope = rememberCoroutineScope() val bookmarks by state.displayedBookmarks.collectAsState() + val bookmarkCount by state.bookmarkCount.collectAsState() val listState = rememberLazyListState() - val hasRefreshed by state.hasRefreshed.collectAsState() - val refresh = { - state.refreshBookmarks { result -> - scope.launch { - when (result) { - is Err -> snackState.show(result.error) - is Ok -> { - delay(1); listState.animateScrollToItem(0) + + LaunchedEffect(true) { + scope.launch { + state.effect.collect { effect -> + when (effect) { + is Effect.RefreshError -> { + snackState.show(effect.error) + } + + Effect.RefreshOk -> { + listState.animateScrollToItem(0) } } } } } - LaunchedEffect(setup_complete) { - if (!hasRefreshed && setup_complete) { - Log.d("DEBUG", "HomeScreen: REFRESHING") - refresh() - } - } - Frame(appBar = { AppBar(page = "Bookmarks", back = null) { - IconButton(onClick = { refresh() }) { + IconButton(onClick = { state.sendEvent(Event.RefreshBookmarks) }) { RefreshIcon(refreshing = state.isRefreshing) } IconButton(onClick = { nav.navigate(PreferencesScreenDestination) }) { @@ -102,78 +84,82 @@ fun HomeScreen( onClick = { nav.navigate(SaveBookmarkScreenDestination) }, content = { Icon(imageVector = Icons.Default.AddLink) }) }, snackState = snackState) { - if (!setup_complete && bookmarks.isEmpty()) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - CircularProgressIndicator() - } - } else { - LazyColumn( - state = listState, - modifier = Modifier.padding(horizontal = padding.standard) - ) { - items(bookmarks, key = { it.id }) { BookmarkRow(it) } + LazyColumn( + state = listState, + modifier = Modifier.padding(horizontal = padding.standard) + ) { + if (bookmarks.isEmpty()) { + item { + Text(text = "No bookmarks... try entering your credentials into the settings") + } + } else { + item { + Text( + text = "Showing $bookmarkCount bookmarks...", + style = typography.labelMedium, + color = colorScheme.secondary + ) + } } + items(bookmarks, key = { it.id }) { BookmarkRow(it) } } } } private suspend fun SnackbarHostState.show(error: APIError) { when (error) { - APIError.CONNECTION -> showSnackbar("Could not connect to LinkDing ") - APIError.AUTH -> showSnackbar("Invalid Credentials") + APIError.NO_CONNECTION -> showSnackbar("Could not connect to LinkDing ") + APIError.INCORRECT_AUTH -> showSnackbar("Invalid Token") + APIError.INCORRECT_ENDPOINT -> showSnackbar("Invalid API Endpoint") + APIError.NO_AUTH_PROVIDED -> {} } } -@ThemePreview -@Composable -fun HomeScreenPreview() { - val state = HomeScreenState(EmptyBookmarkAPI(), EmptyBookmarkSource()) - state.setDisplayedBookmarks( - listOf( - Bookmark( - 1, - "https://garb.com", - website_title = "Pls github | stop making the titles way to large. This is running over 3 lines!!! ludicrous", - website_description = "its a github, also a lame hub, but quite a bit too large? should be concatonated. This runs over way too many lines and should be reduced to fit the correct area", - date_modified = "2024-02-06T00:05:35.570735Z", - tags = listOf("span", "the", "flames") - ), Bookmark( - 2, - "https://www.garb.com", - website_description = "its a github", - date_modified = "2022-02-06T00:05:35.570735Z", - tags = listOf("art", "science", "bullshit") - ), Bookmark( - 3, - "https://garb.com", - website_title = "Pls github | stop making the titles way to large. This is running over 3 lines!!! ludicrous", - website_description = "its a github, also a lame hub, but quite a bit too large? should be concatonated. This runs over way too many lines and should be reduced to fit the correct area", - date_modified = "2024-02-06T00:05:35.570735Z", - tags = listOf("span", "the", "flames") - ), Bookmark( - 4, - "https://www.garb.com", - website_description = "its a github", - date_modified = "2022-02-06T00:05:35.570735Z", - tags = listOf("art", "science", "bullshit") - ), Bookmark( - 5, - "https://www.garb.com", - website_description = "its a github", - date_modified = "2022-02-06T00:05:35.570735Z", - tags = listOf("art", "science", "bullshit") - ) - ) - ) - AppTheme { - HomeScreen( - EmptyDestinationsNavigator, - SnackbarHostState(), - MutableStateFlow(true) - ) { state } - } -} +//@ThemePreview +//@Composable +//fun HomeScreenPreview() { +// val state = HomeScreenState(EmptyBookmarkAPI(), EmptyBookmarkSource()) +// state.setDisplayedBookmarks( +// listOf( +// Bookmark( +// 1, +// "https://garb.com", +// website_title = "Pls github | stop making the titles way to large. This is running over 3 lines!!! ludicrous", +// website_description = "its a github, also a lame hub, but quite a bit too large? should be concatonated. This runs over way too many lines and should be reduced to fit the correct area", +// date_modified = "2024-02-06T00:05:35.570735Z", +// tags = listOf("span", "the", "flames") +// ), Bookmark( +// 2, +// "https://www.garb.com", +// website_description = "its a github", +// date_modified = "2022-02-06T00:05:35.570735Z", +// tags = listOf("art", "science", "bullshit") +// ), Bookmark( +// 3, +// "https://garb.com", +// website_title = "Pls github | stop making the titles way to large. This is running over 3 lines!!! ludicrous", +// website_description = "its a github, also a lame hub, but quite a bit too large? should be concatonated. This runs over way too many lines and should be reduced to fit the correct area", +// date_modified = "2024-02-06T00:05:35.570735Z", +// tags = listOf("span", "the", "flames") +// ), Bookmark( +// 4, +// "https://www.garb.com", +// website_description = "its a github", +// date_modified = "2022-02-06T00:05:35.570735Z", +// tags = listOf("art", "science", "bullshit") +// ), Bookmark( +// 5, +// "https://www.garb.com", +// website_description = "its a github", +// date_modified = "2022-02-06T00:05:35.570735Z", +// tags = listOf("art", "science", "bullshit") +// ) +// ) +// ) +// AppTheme { +// HomeScreen( +// EmptyDestinationsNavigator, +// SnackbarHostState(), +// ) { state } +// } +//} diff --git a/app/src/main/java/org/yrovas/linklater/ui/screens/SaveBookmarkScreen.kt b/app/src/main/java/org/yrovas/linklater/ui/screens/SaveBookmarkScreen.kt index d7b1268..566878f 100644 --- a/app/src/main/java/org/yrovas/linklater/ui/screens/SaveBookmarkScreen.kt +++ b/app/src/main/java/org/yrovas/linklater/ui/screens/SaveBookmarkScreen.kt @@ -113,7 +113,7 @@ fun SaveBookmarkScreen( back() }, ) { - val state = viewModel { state() } + @Suppress("NAME_SHADOWING") val state = viewModel { state() } var isSubmitting by remember { mutableStateOf(false) } val submit = { @@ -168,8 +168,10 @@ fun SaveBookmarkResult( Spacer(Modifier.height(padding.double)) Text( when ((submitResult as Res.Err).error) { - APIError.CONNECTION -> "Failed to connect to LinkDing" - APIError.AUTH -> "Failed to authenticate" + APIError.NO_CONNECTION -> "Failed to connect to LinkDing" + APIError.INCORRECT_AUTH -> "Failed to authenticate" + APIError.NO_AUTH_PROVIDED -> "Failed to authenticate" + APIError.INCORRECT_ENDPOINT -> "Failed to connect" } ) @@ -321,7 +323,7 @@ private fun StyledTagRow( ) } if (unselectedTags.value.isNotEmpty()) { - Text(text = "Add more tags...", style = typography.titleMedium) + Text(text = "Add tags...", style = typography.titleMedium) Row( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/org/yrovas/linklater/ui/state/HomeScreenState.kt b/app/src/main/java/org/yrovas/linklater/ui/state/HomeScreenState.kt index 118aa31..57615a9 100644 --- a/app/src/main/java/org/yrovas/linklater/ui/state/HomeScreenState.kt +++ b/app/src/main/java/org/yrovas/linklater/ui/state/HomeScreenState.kt @@ -1,60 +1,103 @@ package org.yrovas.linklater.ui.state -import androidx.lifecycle.ViewModel +import android.util.Log import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import org.yrovas.linklater.data.Bookmark -import org.yrovas.linklater.data.local.BookmarkDataSource -import org.yrovas.linklater.data.remote.BookmarkAPI import org.yrovas.linklater.domain.APIError -import org.yrovas.linklater.domain.Res +import org.yrovas.linklater.domain.BookmarkAPI +import org.yrovas.linklater.domain.BookmarkDataSource +import org.yrovas.linklater.domain.errorOrThrow import org.yrovas.linklater.domain.ifOk +import org.yrovas.linklater.domain.isOk + +const val TAG = "DEBUG/state" @Inject class HomeScreenState( private val api: BookmarkAPI, private val bookmarkSource: BookmarkDataSource, -) : ViewModel() { - fun refreshBookmarks(onRefresh: suspend (Res) -> Unit) { - _isRefreshing.update { true } - _hasRefreshed.update { true } - viewModelScope.launch(Dispatchers.IO) { - val res = api.getBookmarks(page = 0) -// res.ok { setDisplayedBookmarks(it) } - _isRefreshing.update { false } - onRefresh(res) +) : ScreenState() { - res.ifOk { - bookmarkSource.insertBookmarks(it) - } - } + sealed interface Event : ScreenEvent { + data object RefreshBookmarks : Event } - private val _isRefreshing = MutableStateFlow(false) - var isRefreshing = _isRefreshing.asStateFlow() + sealed interface Effect : ScreenEffect { + data class RefreshError(val error: APIError) : Effect + data object RefreshOk : Effect + } - private val _hasRefreshed = MutableStateFlow(false) - var hasRefreshed = _hasRefreshed.asStateFlow() + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() private val _displayedBookmarks = MutableStateFlow(listOf()) val displayedBookmarks = _displayedBookmarks.asStateFlow() - fun setDisplayedBookmarks(bookmarks: List) { - _displayedBookmarks.update { bookmarks } + private val _bookmarkCount = MutableStateFlow(0) + val bookmarkCount = _bookmarkCount.asStateFlow() + + private fun refreshBookmarks() { + _isRefreshing.update { true } + viewModelScope.launch(Dispatchers.IO) { + val res = api.getBookmarks(page = 0) + sendEffect { + if (res.isOk) Effect.RefreshOk + else Effect.RefreshError(res.errorOrThrow()) + } + _isRefreshing.update { false } + res.ifOk { bookmarkSource.insertBookmarks(it) } + } } +// fun fullSync() { +// viewModelScope.launch(Dispatchers.IO) { +// var res = api.getBookmarks(0) +// var page = 0 +// while (true) when (res) { +// is Res.Err -> break +// is Res.Ok -> { +// if (res.data.isEmpty()) { +// break +// } +// Log.d( +// "DEBUG", "fullSync: FETCHED ${res.data} from page $page" +// ) +// bookmarkSource.insertBookmarks(res.data) +// res = api.getBookmarks(page++) +// } +// } +// } +// } + init { viewModelScope.launch(Dispatchers.IO) { - bookmarkSource.getBookmarks().collect { - _displayedBookmarks.update { list -> - (list + it).distinct().sortedByDescending { it.date_modified } + bookmarkSource.getBookmarks().collect { bookmarks -> + _displayedBookmarks.update { + bookmarks.distinct().sortedByDescending { it.date_modified } + } + _bookmarkCount.emit(bookmarkSource.getBookmarkCount()) + } + } + viewModelScope.launch(Dispatchers.IO) { + // when authenticated refresh + api.authProvided.transformWhile { emit(it); !it }.collect { + if (it) { + refreshBookmarks() } } } } + + override fun handleEvent(event: Event) { + when (event) { + Event.RefreshBookmarks -> refreshBookmarks() + } + } } diff --git a/app/src/main/java/org/yrovas/linklater/ui/state/PreferencesScreenState.kt b/app/src/main/java/org/yrovas/linklater/ui/state/PreferencesScreenState.kt index 17fd0df..9d5346e 100644 --- a/app/src/main/java/org/yrovas/linklater/ui/state/PreferencesScreenState.kt +++ b/app/src/main/java/org/yrovas/linklater/ui/state/PreferencesScreenState.kt @@ -11,27 +11,27 @@ import me.tatarka.inject.annotations.Inject import org.yrovas.linklater.data.LocalBookmark import org.yrovas.linklater.data.local.PrefDataStore import org.yrovas.linklater.data.local.Prefs -import org.yrovas.linklater.data.remote.BookmarkAPI +import org.yrovas.linklater.domain.BookmarkAPI import org.yrovas.linklater.intoTags -//@Provides -//fun providePreferencesScreenState(appViewModel: () -> AppViewModelImpl): PreferencesScreenState = -// PreferencesScreenState(appViewModel) - @Inject class PreferencesScreenState( private val bookmarkAPI: BookmarkAPI, private val prefStore: PrefDataStore, ) : ViewModel() { - private val _bookmarkEndpoint: MutableStateFlow = MutableStateFlow("") + private val _bookmarkEndpoint = MutableStateFlow("") var bookmarkEndpoint = _bookmarkEndpoint.asStateFlow() - private val _bookmarkAPIToken: MutableStateFlow = MutableStateFlow("") + + private val _bookmarkAPIToken = MutableStateFlow("") var bookmarkAPIToken = _bookmarkAPIToken.asStateFlow() - private fun saveBookmarkConf( - url: String? = null, - token: String? = null, - ) { + private val _tag_names: MutableStateFlow = MutableStateFlow("") + var tag_names = _tag_names.asStateFlow() + + private val _defaultBookmark = MutableStateFlow(LocalBookmark("")) + var defaultBookmark = _defaultBookmark.asStateFlow() + + private fun saveBookmarkConf(url: String? = null, token: String? = null) { if (url != null) _bookmarkEndpoint.update { url } if (token != null) _bookmarkAPIToken.update { token } @@ -52,12 +52,6 @@ class PreferencesScreenState( saveBookmarkConf(token = token) } - private val _tag_names: MutableStateFlow = MutableStateFlow("") - var tag_names = _tag_names.asStateFlow() - private val _defaultBookmark: MutableStateFlow = - MutableStateFlow(LocalBookmark("")) - var defaultBookmark = _defaultBookmark.asStateFlow() - fun saveDefaultBookmark( tag_names: String? = null, unread: Boolean? = null, @@ -96,10 +90,12 @@ class PreferencesScreenState( this._tag_names.update { tag_names ?: "" } val tags = this.tag_names.value.intoTags() _defaultBookmark.update { - it.withUpdates(is_archived = is_archived, + it.withUpdates( + is_archived = is_archived, unread = unread, shared = shared, - tags = tags.ifEmpty { null }) + tags = tags + ) } } diff --git a/app/src/main/java/org/yrovas/linklater/ui/state/SaveBookmarkScreenState.kt b/app/src/main/java/org/yrovas/linklater/ui/state/SaveBookmarkScreenState.kt index bead2ff..d18dbe2 100644 --- a/app/src/main/java/org/yrovas/linklater/ui/state/SaveBookmarkScreenState.kt +++ b/app/src/main/java/org/yrovas/linklater/ui/state/SaveBookmarkScreenState.kt @@ -10,16 +10,15 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import org.yrovas.linklater.checkURL import org.yrovas.linklater.data.LocalBookmark -import org.yrovas.linklater.data.local.BookmarkDataSource -import org.yrovas.linklater.data.local.EmptyBookmarkSource import org.yrovas.linklater.data.local.PrefDataStore import org.yrovas.linklater.data.local.Prefs -import org.yrovas.linklater.data.local.TagDataSource -import org.yrovas.linklater.data.remote.BookmarkAPI import org.yrovas.linklater.domain.APIError +import org.yrovas.linklater.domain.BookmarkAPI +import org.yrovas.linklater.domain.BookmarkDataSource import org.yrovas.linklater.domain.Err import org.yrovas.linklater.domain.Ok import org.yrovas.linklater.domain.Res +import org.yrovas.linklater.domain.TagDataSource import org.yrovas.linklater.intoTags @Inject @@ -30,10 +29,25 @@ class SaveBookmarkScreenState( private val bookmarkSource: BookmarkDataSource, ) : ViewModel() { - private val _bookmarkToSave: MutableStateFlow = - MutableStateFlow(LocalBookmark("")) + private val _bookmarkToSave = MutableStateFlow(LocalBookmark("")) var bookmarkToSave = _bookmarkToSave.asStateFlow() + private val _tagNames = MutableStateFlow("") + var tagNames = _tagNames.asStateFlow() + + private val _tags: MutableStateFlow> = MutableStateFlow(listOf()) + var tags = _tags.asStateFlow() + + private val _selectedTags = MutableStateFlow(emptyList()) + var selectedTags = _selectedTags.asStateFlow() + + private val _showPaste = MutableStateFlow(bookmarkToSave.value.url.isBlank()) + var showPaste = _showPaste.asStateFlow() + + private val _submitResult: MutableStateFlow?> = + MutableStateFlow(null) + var submitResult = _submitResult.asStateFlow() + fun updateBookmark( url: String? = null, title: String? = null, @@ -58,24 +72,10 @@ class SaveBookmarkScreenState( } } - private val _showPaste: MutableStateFlow = - MutableStateFlow(bookmarkToSave.value.url.isBlank()) - var showPaste = _showPaste.asStateFlow() - - private val _tagNames: MutableStateFlow = MutableStateFlow("") - var tagNames = _tagNames.asStateFlow() - fun updateTagNames(tagNames: String) { _tagNames.update { tagNames } } - private val _tags: MutableStateFlow> = MutableStateFlow(listOf()) - var tags = _tags.asStateFlow() - - private val _selectedTags: MutableStateFlow> = - MutableStateFlow(emptyList()) - var selectedTags = _selectedTags.asStateFlow() - fun toggleSelectTag(tag: String) { _selectedTags.update { (if (it.contains(tag)) it - tag @@ -83,12 +83,8 @@ class SaveBookmarkScreenState( } } - private val _submitResult: MutableStateFlow?> = - MutableStateFlow(null) - var submitResult = _submitResult.asStateFlow() private fun setSubmitResult(result: Res) = _submitResult.update { result } -// fun clearSubmitResult() {} fun submitBookmark() { val tags = diff --git a/app/src/main/java/org/yrovas/linklater/ui/state/ScreenState.kt b/app/src/main/java/org/yrovas/linklater/ui/state/ScreenState.kt new file mode 100644 index 0000000..76964ee --- /dev/null +++ b/app/src/main/java/org/yrovas/linklater/ui/state/ScreenState.kt @@ -0,0 +1,45 @@ +package org.yrovas.linklater.ui.state + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +interface ScreenEvent + +interface ScreenEffect + +abstract class ScreenState : + ViewModel() { + private val _event: MutableSharedFlow = MutableSharedFlow() + protected val event = _event.asSharedFlow() + private val _effect: Channel = Channel() + val effect = _effect.receiveAsFlow() // consumeAsFlow? + + private fun subscribeEvents() { + viewModelScope.launch { + event.collect { + handleEvent(it) + } + } + } + + fun sendEvent(event: Event) { + val newEvent = event + viewModelScope.launch { _event.emit(newEvent) } + } + + protected fun sendEffect(builder: () -> Effect) { + val effectValue = builder() + viewModelScope.launch { _effect.send(effectValue) } + } + + abstract fun handleEvent(event: Event) + + init { + subscribeEvents() + } +} diff --git a/app/src/main/sqldelight/linklater/BookmarkTags.sq b/app/src/main/sqldelight/linklater/BookmarkTags.sq index f4cab15..1120037 100644 --- a/app/src/main/sqldelight/linklater/BookmarkTags.sq +++ b/app/src/main/sqldelight/linklater/BookmarkTags.sq @@ -72,6 +72,10 @@ VALUES (?,?,?,?,?,?,?,?,?,?,?,?); SELECT last_insert_rowid(); } +countBookmarks: +SELECT COUNT(*) FROM bookmarkEntity +; + deleteBookmarkByID: DELETE FROM bookmarkEntity WHERE id = :id;