diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 1c0db52d..c763172c 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { kapt(libs.dagger.hilt.compiler) implementation(projects.api) implementation(projects.common) + implementation(projects.database) implementation(libs.accompanist.insets) implementation(libs.accompanist.swiperefresh) implementation(libs.accompanist.sysuicontroller) @@ -21,6 +22,7 @@ dependencies { implementation(libs.androidx.lifecycle.compose) implementation(libs.androidx.paging.compose) implementation(libs.dagger.hilt.android) + implementation(libs.sqldelight.extensions.coroutines) implementation(libs.retrofit.moshiConverter) } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt index b10d0246..8e9c452f 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue 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.geometry.Offset @@ -35,6 +36,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.common.theme.LobstersTheme import dev.msfjarvis.claw.common.urllauncher.UrlLauncher +import kotlinx.coroutines.launch private const val ScrollDelta = 50 @@ -47,6 +49,7 @@ fun LobstersApp( val systemUiController = rememberSystemUiController() val scaffoldState = rememberScaffoldState() val listState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() var isFabVisible by remember { mutableStateOf(true) } val nestedScrollConnection = remember { object : NestedScrollConnection { @@ -97,6 +100,8 @@ fun LobstersApp( items = items, launchUrl = urlLauncher::launch, listState = listState, + isSaved = viewModel::isPostSaved, + toggleSave = { coroutineScope.launch { viewModel.toggleSave(it) } }, modifier = Modifier.padding(top = 16.dp).nestedScroll(nestedScrollConnection), ) } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/NetworkPosts.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/NetworkPosts.kt index e2d0d25a..75edc5a2 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/NetworkPosts.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/NetworkPosts.kt @@ -4,6 +4,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.unit.dp import androidx.paging.compose.LazyPagingItems @@ -11,26 +16,34 @@ import androidx.paging.compose.items import dev.msfjarvis.claw.android.ext.toDbModel import dev.msfjarvis.claw.api.model.LobstersPost import dev.msfjarvis.claw.common.posts.LobstersCard +import dev.msfjarvis.claw.database.local.SavedPost +import kotlinx.coroutines.launch @Composable fun NetworkPosts( items: LazyPagingItems, listState: LazyListState, launchUrl: (String) -> Unit, + isSaved: suspend (SavedPost) -> Boolean, + toggleSave: (SavedPost) -> Unit, modifier: Modifier = Modifier, ) { + val coroutineScope = rememberCoroutineScope() LazyColumn( state = listState, modifier = Modifier.then(modifier), ) { items(items) { item -> if (item != null) { + val dbModel = item.toDbModel() + var saved by remember(dbModel) { mutableStateOf(false) } + coroutineScope.launch { saved = isSaved(dbModel) } LobstersCard( - post = item.toDbModel(), - isSaved = false, + post = dbModel, + isSaved = saved, viewPost = { launchUrl(item.url.ifEmpty { item.commentsUrl }) }, viewComments = { launchUrl(item.commentsUrl) }, - toggleSave = {}, + toggleSave = { toggleSave(dbModel) }, modifier = Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp), ) } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt index 9de12d20..d24b16d9 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt @@ -6,15 +6,24 @@ import androidx.paging.PagingConfig import dagger.hilt.android.lifecycle.HiltViewModel import dev.msfjarvis.claw.android.paging.LobstersPagingSource import dev.msfjarvis.claw.api.LobstersApi +import dev.msfjarvis.claw.database.local.SavedPost import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.mapLatest +@OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class ClawViewModel @Inject constructor( api: LobstersApi, + private val repository: SavedPostsRepository, ) : ViewModel() { var lastPagingSource: LobstersPagingSource? = null + private val savedPosts = flow { repository.savedPosts.collect { emit(it) } } private val pager = Pager(PagingConfig(20)) { LobstersPagingSource(api::getHottestPosts).also { lastPagingSource = it } @@ -23,6 +32,20 @@ constructor( val pagerFlow get() = pager.flow + suspend fun isPostSaved(post: SavedPost): Boolean { + return savedPosts.mapLatest { posts -> post in posts }.last() + } + + suspend fun toggleSave(post: SavedPost) { + val saved = isPostSaved(post) + println("saved=$saved") + if (saved) { + repository.removePost(post) + } else { + repository.savePost(post) + } + } + fun reloadPosts() { lastPagingSource?.invalidate() } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/SavedPostsRepository.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/SavedPostsRepository.kt new file mode 100644 index 00000000..3e84ba00 --- /dev/null +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/SavedPostsRepository.kt @@ -0,0 +1,28 @@ +package dev.msfjarvis.claw.android.viewmodel + +import com.squareup.sqldelight.runtime.coroutines.asFlow +import com.squareup.sqldelight.runtime.coroutines.mapToList +import dev.msfjarvis.claw.database.LobstersDatabase +import dev.msfjarvis.claw.database.local.SavedPost +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SavedPostsRepository +@Inject +constructor( + database: LobstersDatabase, +) { + private val savedPostQueries = database.savedPostQueries + val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList() + + suspend fun savePost(post: SavedPost) { + println("Saving post: ${post.shortId}") + withContext(Dispatchers.IO) { savedPostQueries.insertOrReplacePost(post) } + } + + suspend fun removePost(post: SavedPost) { + println("Removing post: ${post.shortId}") + withContext(Dispatchers.IO) { savedPostQueries.deletePost(post.shortId) } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61bbb2bc..3cea7029 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,5 +43,6 @@ retrofit-moshiConverter = { module = "com.squareup.retrofit2:converter-moshi", v sqldelight-jvmDriver = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-androidDriver = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } +sqldelight-extensions-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions-jvm", version.ref = "sqldelight" } testing-mockWebServer = "com.squareup.okhttp3:mockwebserver3-junit4:5.0.0-alpha.2"