diff --git a/.idea/misc.xml b/.idea/misc.xml
index 35279bc..49ffb09 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -14,6 +14,13 @@
+
+
+
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3b11f86..3a769d9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -61,6 +61,7 @@ dependencies {
implementation(libs.lifecycle)
implementation(libs.hilt.android.core)
kapt(libs.hilt.android.kapt)
+ implementation(libs.hilt.navigation.compose)
implementation(libs.hilt.work.core)
kapt(libs.hilt.work.kapt)
implementation(libs.work.core)
diff --git a/app/src/main/java/com/wbrawner/nanoflux/SyncWorker.kt b/app/src/main/java/com/wbrawner/nanoflux/SyncWorker.kt
index b8bd576..7ccdb02 100644
--- a/app/src/main/java/com/wbrawner/nanoflux/SyncWorker.kt
+++ b/app/src/main/java/com/wbrawner/nanoflux/SyncWorker.kt
@@ -12,6 +12,7 @@ import com.wbrawner.nanoflux.network.repository.IconRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.runBlocking
+import timber.log.Timber
import javax.inject.Inject
@HiltWorker
@@ -33,14 +34,9 @@ class SyncWorker @AssistedInject constructor(
override fun doWork(): Result {
return runBlocking {
+ Timber.v("Unread entryRepo: ${entryRepository}r")
try {
- categoryRepository.getAll(true)
- feedRepository.getAll(true).forEach {
- if (it.feed.iconId != null) {
- iconRepository.getFeedIcon(it.feed.id)
- }
- }
- entryRepository.getAll(true)
+ syncAll(categoryRepository, feedRepository, iconRepository, entryRepository)
Result.success()
} catch (e: Exception) {
Result.failure(Data.Builder().putString("message", e.message?: "Unknown failure").build())
diff --git a/app/src/main/java/com/wbrawner/nanoflux/Utils.kt b/app/src/main/java/com/wbrawner/nanoflux/Utils.kt
new file mode 100644
index 0000000..2d0f776
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/nanoflux/Utils.kt
@@ -0,0 +1,23 @@
+package com.wbrawner.nanoflux
+
+import com.wbrawner.nanoflux.network.repository.CategoryRepository
+import com.wbrawner.nanoflux.network.repository.EntryRepository
+import com.wbrawner.nanoflux.network.repository.FeedRepository
+import com.wbrawner.nanoflux.network.repository.IconRepository
+
+suspend fun syncAll(
+ categoryRepository: CategoryRepository,
+ feedRepository: FeedRepository,
+ iconRepository: IconRepository,
+ entryRepository: EntryRepository
+) {
+ // The order of operations is important here to prevent the DAOs from returning incomplete
+ // objects. E.g. The EntryDao returns an EntryAndFeed, which in turn contains a FeedCategoryIcon
+ categoryRepository.getAll(true)
+ feedRepository.getAll(true).forEach {
+ if (it.feed.iconId != null) {
+ iconRepository.getFeedIcon(it.feed.id)
+ }
+ }
+ entryRepository.getAll(true, entryRepository.getLatestId())
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/EntryViewModel.kt b/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/EntryViewModel.kt
new file mode 100644
index 0000000..c063044
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/EntryViewModel.kt
@@ -0,0 +1,56 @@
+package com.wbrawner.nanoflux.data.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.wbrawner.nanoflux.network.repository.CategoryRepository
+import com.wbrawner.nanoflux.network.repository.EntryRepository
+import com.wbrawner.nanoflux.network.repository.FeedRepository
+import com.wbrawner.nanoflux.storage.model.Entry
+import com.wbrawner.nanoflux.storage.model.EntryAndFeed
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import timber.log.Timber
+import javax.inject.Inject
+
+@HiltViewModel
+class EntryViewModel @Inject constructor(
+ private val feedRepository: FeedRepository,
+ private val categoryRepository: CategoryRepository,
+ private val entryRepository: EntryRepository,
+ private val logger: Timber.Tree
+) : ViewModel() {
+ private val _loading = MutableStateFlow(true)
+ val loading = _loading.asStateFlow()
+ private val _errorMessage = MutableStateFlow(null)
+ val errorMessage = _errorMessage.asStateFlow()
+ private val _entry = MutableStateFlow(null)
+ val entry = _entry.asStateFlow()
+
+ suspend fun loadEntry(id: Long) {
+ withContext(Dispatchers.IO) {
+ _loading.value = true
+ _errorMessage.value = null
+ try {
+ val entry = entryRepository.getEntry(id)
+ _entry.value = entry
+ _loading.value = false
+ entryRepository.markEntryRead(entry.entry)
+ } catch (e: Exception) {
+ _errorMessage.value = e.message?: "Error fetching entry"
+ _loading.value = false
+ logger.e(e)
+ }
+ }
+ }
+
+ fun dismissError() {
+ _errorMessage.value = null
+ }
+
+ // TODO: Get Base URL
+ fun getShareUrl(entry: Entry) = "baseUrl/${entry.shareCode}"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/MainViewModel.kt b/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/MainViewModel.kt
index 08c054b..269cb73 100644
--- a/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/MainViewModel.kt
@@ -5,7 +5,10 @@ import androidx.lifecycle.viewModelScope
import com.wbrawner.nanoflux.network.repository.CategoryRepository
import com.wbrawner.nanoflux.network.repository.EntryRepository
import com.wbrawner.nanoflux.network.repository.FeedRepository
+import com.wbrawner.nanoflux.network.repository.IconRepository
+import com.wbrawner.nanoflux.storage.model.Entry
import com.wbrawner.nanoflux.storage.model.EntryAndFeed
+import com.wbrawner.nanoflux.syncAll
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -18,29 +21,16 @@ import javax.inject.Inject
class MainViewModel @Inject constructor(
private val feedRepository: FeedRepository,
private val categoryRepository: CategoryRepository,
+ private val iconRepository: IconRepository,
private val entryRepository: EntryRepository,
private val logger: Timber.Tree
) : ViewModel() {
- private val _state = MutableStateFlow(FeedState.Loading)
- val state = _state.asStateFlow()
- fun loadUnread() {
- viewModelScope.launch(Dispatchers.IO) {
- try {
- var entries = entryRepository.getAllUnread()
- if (entries.isEmpty()) entries = entryRepository.getAllUnread(true)
- val state = if (entries.isEmpty()) FeedState.Empty else FeedState.Success(entries)
- _state.emit(state)
- } catch (e: Exception) {
- _state.emit(FeedState.Failed(e.localizedMessage))
+ init {
+ viewModelScope.launch {
+ if (entryRepository.getCount() == 0L) {
+ syncAll(categoryRepository, feedRepository, iconRepository, entryRepository)
}
}
}
-
- sealed class FeedState {
- object Loading : FeedState()
- class Failed(val errorMessage: String?): FeedState()
- object Empty: FeedState()
- class Success(val entries: List): FeedState()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/UnreadViewModel.kt b/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/UnreadViewModel.kt
new file mode 100644
index 0000000..d22402b
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/UnreadViewModel.kt
@@ -0,0 +1,40 @@
+package com.wbrawner.nanoflux.data.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.wbrawner.nanoflux.network.repository.CategoryRepository
+import com.wbrawner.nanoflux.network.repository.EntryRepository
+import com.wbrawner.nanoflux.network.repository.FeedRepository
+import com.wbrawner.nanoflux.storage.model.Entry
+import com.wbrawner.nanoflux.storage.model.EntryAndFeed
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import javax.inject.Inject
+
+@HiltViewModel
+class UnreadViewModel @Inject constructor(
+ private val feedRepository: FeedRepository,
+ private val categoryRepository: CategoryRepository,
+ private val entryRepository: EntryRepository,
+ private val logger: Timber.Tree
+) : ViewModel() {
+ private val _loading = MutableStateFlow(false)
+ val loading = _loading.asStateFlow()
+ private val _errorMessage = MutableStateFlow(null)
+ val errorMessage = _errorMessage.asStateFlow()
+ val entries = entryRepository.observeUnread()
+
+ init {
+ logger.v("Unread entryRepo: ${entryRepository}r")
+ }
+
+ fun dismissError() {
+ _errorMessage.value = null
+ }
+
+ // TODO: Get Base URL
+ fun getShareUrl(entry: Entry) = "baseUrl/${entry.shareCode}"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/nanoflux/ui/Entries.kt b/app/src/main/java/com/wbrawner/nanoflux/ui/Entries.kt
index bfdf86f..33767db 100644
--- a/app/src/main/java/com/wbrawner/nanoflux/ui/Entries.kt
+++ b/app/src/main/java/com/wbrawner/nanoflux/ui/Entries.kt
@@ -34,6 +34,7 @@ fun EntryList(
onStarClicked: (entry: Entry) -> Unit,
onExternalLinkClicked: (entry: Entry) -> Unit
) {
+ // TODO: Add pull to refresh
LazyColumn {
items(entries) { entry ->
EntryListItem(
@@ -209,8 +210,9 @@ private const val MILLIS_IN_WEEK = 604_800_000.0
private const val MILLIS_IN_MONTH = 2_628_000_000.0
private const val MILLIS_IN_YEAR = 31_540_000_000.0
+val now = GregorianCalendar().timeInMillis
fun Date.timeSince(): String {
- val difference = System.currentTimeMillis() - time
+ val difference = now - time
return when {
difference >= MILLIS_IN_YEAR -> "${(difference / MILLIS_IN_YEAR).roundToInt()} years ago"
difference >= MILLIS_IN_MONTH -> "${(difference / MILLIS_IN_MONTH).roundToInt()} months ago"
diff --git a/app/src/main/java/com/wbrawner/nanoflux/ui/EntryScreen.kt b/app/src/main/java/com/wbrawner/nanoflux/ui/EntryScreen.kt
new file mode 100644
index 0000000..32d1141
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/nanoflux/ui/EntryScreen.kt
@@ -0,0 +1,69 @@
+package com.wbrawner.nanoflux.ui
+
+import android.text.Html
+import android.widget.TextView
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.wbrawner.nanoflux.data.viewmodel.EntryViewModel
+import com.wbrawner.nanoflux.storage.model.EntryAndFeed
+
+@Composable
+fun EntryScreen(entryId: Long, entryViewModel: EntryViewModel) {
+ val loading by entryViewModel.loading.collectAsState()
+ val errorMessage by entryViewModel.errorMessage.collectAsState()
+ val entry by entryViewModel.entry.collectAsState()
+ LaunchedEffect(key1 = entryId) {
+ entryViewModel.loadEntry(entryId)
+ }
+ if (loading) {
+ CircularProgressIndicator()
+ }
+ errorMessage?.let {
+ Text("Unable to load entry: $it")
+ }
+ entry?.let {
+ Entry(it)
+ }
+}
+
+@Composable
+fun Entry(entry: EntryAndFeed) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ ) {
+ EntryListItem(
+ entry = entry.entry,
+ feed = entry.feed,
+ onEntryItemClicked = { /*TODO*/ },
+ onFeedClicked = { /*TODO*/ },
+ onToggleReadClicked = { /*TODO*/ },
+ onStarClicked = { /*TODO*/ },
+ onExternalLinkClicked = { /* TODO */ }
+ )
+ // Adds view to Compose
+ AndroidView(
+ modifier = Modifier.fillMaxSize().padding(16.dp),
+ factory = { context ->
+ TextView(context).apply {
+ text = Html.fromHtml(entry.entry.content)
+ }
+ },
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/nanoflux/ui/MainScreen.kt b/app/src/main/java/com/wbrawner/nanoflux/ui/MainScreen.kt
index e8d7dea..9e17c0f 100644
--- a/app/src/main/java/com/wbrawner/nanoflux/ui/MainScreen.kt
+++ b/app/src/main/java/com/wbrawner/nanoflux/ui/MainScreen.kt
@@ -5,18 +5,20 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navArgument
import androidx.navigation.compose.rememberNavController
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
@@ -25,6 +27,7 @@ import com.wbrawner.nanoflux.SyncWorker
import com.wbrawner.nanoflux.data.viewmodel.AuthViewModel
import com.wbrawner.nanoflux.data.viewmodel.MainViewModel
import kotlinx.coroutines.launch
+import java.util.*
@Composable
fun MainScreen(
@@ -32,30 +35,24 @@ fun MainScreen(
mainViewModel: MainViewModel = viewModel()
) {
val context = LocalContext.current
- LaunchedEffect(key1 = authViewModel.state) {
- val workRequest = OneTimeWorkRequestBuilder().build()
- WorkManager.getInstance(context).enqueue(workRequest)
- mainViewModel.loadUnread()
+ LaunchedEffect(key1 = context) {
+ WorkManager.getInstance(context)
+ .enqueue(OneTimeWorkRequestBuilder().build())
}
- val state = mainViewModel.state.collectAsState()
val navController = rememberNavController()
MainScaffold(
- state.value,
navController,
- {
-
- },
- {},
- {},
- {},
- {},
- {}
+ { navController.navigate("unread") },
+ { navController.navigate("starred") },
+ { navController.navigate("history") },
+ { navController.navigate("feeds") },
+ { navController.navigate("categories") },
+ { navController.navigate("settings") },
)
}
@Composable
fun MainScaffold(
- state: MainViewModel.FeedState,
navController: NavHostController,
onUnreadClicked: () -> Unit,
onStarredClicked: () -> Unit,
@@ -64,40 +61,60 @@ fun MainScaffold(
onCategoriesClicked: () -> Unit,
onSettingsClicked: () -> Unit,
) {
- val scaffoldState = rememberScaffoldState()
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState)
val coroutineScope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
- TopAppBar(navigationIcon = {
- IconButton(onClick = { coroutineScope.launch { scaffoldState.drawerState.open() } }) {
- Icon(
- imageVector = Icons.Default.Menu,
- contentDescription = "Menu"
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { coroutineScope.launch { scaffoldState.drawerState.open() } }) {
+ Icon(
+ imageVector = Icons.Default.Menu,
+ contentDescription = "Menu"
+ )
+ }
+ },
+ title = {
+ Text(
+ text = navController.currentDestination?.displayName?.capitalize(Locale.ENGLISH)
+ ?: "Unread"
)
- }
- }, title = { Text(text = "Unread") })
+ })
},
drawerContent = {
DrawerButton(onClick = onUnreadClicked, icon = Icons.Default.Email, text = "Unread")
DrawerButton(onClick = onStarredClicked, icon = Icons.Default.Star, text = "Starred")
- DrawerButton(onClick = onHistoryClicked, icon = Icons.Default.DateRange, text = "History")
+ DrawerButton(
+ onClick = onHistoryClicked,
+ icon = Icons.Default.DateRange,
+ text = "History"
+ )
DrawerButton(onClick = onFeedsClicked, icon = Icons.Default.List, text = "Feeds")
- DrawerButton(onClick = onCategoriesClicked, icon = Icons.Default.Info, text = "Categories")
- DrawerButton(onClick = onSettingsClicked, icon = Icons.Default.Settings, text = "Settings")
+ DrawerButton(
+ onClick = onCategoriesClicked,
+ icon = Icons.Default.Info,
+ text = "Categories"
+ )
+ DrawerButton(
+ onClick = onSettingsClicked,
+ icon = Icons.Default.Settings,
+ text = "Settings"
+ )
}
) {
- when (state) {
- is MainViewModel.FeedState.Loading -> CircularProgressIndicator()
- is MainViewModel.FeedState.Failed -> Text("TODO: Failed to load entries: ${state.errorMessage}")
- is MainViewModel.FeedState.Success -> EntryList(
- entries = state.entries,
- onEntryItemClicked = { /*TODO*/ },
- onFeedClicked = { /*TODO*/ },
- onToggleReadClicked = { /*TODO*/ },
- onStarClicked = { /*TODO*/ }) {
+ // TODO: Extract routes to constants
+ NavHost(navController, startDestination = "unread") {
+ composable("unread") {
+ UnreadScreen(navController, snackbarHostState, hiltViewModel())
+ }
+ composable(
+ "entries/{entryId}",
+ arguments = listOf(navArgument("entryId") { type = NavType.LongType })
+ ) {
+ EntryScreen(it.arguments!!.getLong("entryId"), hiltViewModel())
}
- is MainViewModel.FeedState.Empty -> Text("TODO: No entries")
}
}
}
@@ -132,6 +149,6 @@ fun DrawerButton(onClick: () -> Unit, icon: ImageVector, text: String) {
fun MainScaffold_Preview() {
NanofluxApp {
val navController = rememberNavController()
- MainScaffold(MainViewModel.FeedState.Loading, navController, {}, {}, {}, {}, {}, {})
+ MainScaffold(navController, {}, {}, {}, {}, {}, {})
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/nanoflux/ui/UnreadScreen.kt b/app/src/main/java/com/wbrawner/nanoflux/ui/UnreadScreen.kt
new file mode 100644
index 0000000..ac50c82
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/nanoflux/ui/UnreadScreen.kt
@@ -0,0 +1,46 @@
+package com.wbrawner.nanoflux.ui
+
+import androidx.compose.material.*
+import androidx.compose.runtime.*
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import com.wbrawner.nanoflux.data.viewmodel.UnreadViewModel
+import kotlinx.coroutines.launch
+
+@Composable
+fun UnreadScreen(
+ navController: NavController,
+ snackbarHostState: SnackbarHostState,
+ unreadViewModel: UnreadViewModel = viewModel()
+) {
+ val loading by unreadViewModel.loading.collectAsState()
+ val errorMessage by unreadViewModel.errorMessage.collectAsState()
+ val entries by unreadViewModel.entries.collectAsState(emptyList())
+ val coroutineScope = rememberCoroutineScope()
+ if (loading) {
+ CircularProgressIndicator()
+ }
+ errorMessage?.let {
+ coroutineScope.launch {
+ when (snackbarHostState.showSnackbar(it, "Retry")) {
+// SnackbarResult.ActionPerformed -> unreadViewModel.loadUnread()
+ else -> unreadViewModel.dismissError()
+ }
+ }
+ }
+ if (entries.isEmpty()) {
+ Text("TODO: No entries")
+ } else {
+ EntryList(
+ entries = entries,
+ onEntryItemClicked = {
+ navController.navigate("entries/${it.id}")
+ },
+ onFeedClicked = {
+ navController.navigate("feeds/${it.id}")
+ },
+ onToggleReadClicked = { /*TODO*/ },
+ onStarClicked = { /*TODO*/ }) {
+ }
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 27807ab..e23cf9d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -32,6 +32,7 @@ preference = { module = "androidx.preference:preference-ktx", version = "1.1.1"
lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version = "2.3.1" }
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
hilt-android-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt-android" }
+hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0-alpha02" }
hilt-work-core = { module = "androidx.hilt:hilt-work", version.ref = "hilt-work" }
hilt-work-kapt = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt-work" }
junit = { module = "junit:junit", version = "4.12" }
diff --git a/network/src/main/java/com/wbrawner/nanoflux/network/MinifluxApiService.kt b/network/src/main/java/com/wbrawner/nanoflux/network/MinifluxApiService.kt
index 8c26d5a..0270c2b 100644
--- a/network/src/main/java/com/wbrawner/nanoflux/network/MinifluxApiService.kt
+++ b/network/src/main/java/com/wbrawner/nanoflux/network/MinifluxApiService.kt
@@ -5,6 +5,7 @@ import com.wbrawner.nanoflux.network.repository.PREF_KEY_AUTH_TOKEN
import com.wbrawner.nanoflux.storage.model.*
import io.ktor.client.*
import io.ktor.client.engine.cio.*
+import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
@@ -166,6 +167,9 @@ data class CreateFeedResponse(@SerialName("feed_id") val feedId: Long)
@Serializable
data class EntryResponse(val total: Long, val entries: List)
+@Serializable
+data class UpdateEntryRequest(@SerialName("entry_ids") val entryIds: List, val status: Entry.Status)
+
@Suppress("unused")
class KtorMinifluxApiService @Inject constructor(
private val sharedPreferences: SharedPreferences,
@@ -185,7 +189,7 @@ class KtorMinifluxApiService @Inject constructor(
}
}
- level = LogLevel.ALL
+ level = LogLevel.HEADERS
}
}
private val _baseUrl: AtomicReference = AtomicReference()
@@ -276,6 +280,7 @@ class KtorMinifluxApiService @Inject constructor(
}
override suspend fun updateFeed(id: Long, feed: FeedJson): Feed = client.put(url("feeds/$id")) {
+ contentType(ContentType.Application.Json)
body = feed
}
@@ -362,10 +367,8 @@ class KtorMinifluxApiService @Inject constructor(
override suspend fun updateEntries(entryIds: List, status: Entry.Status): HttpResponse =
client.put(url("entries")) {
-// body = @Serializable object {
-// val entry_ids = entryIds
-// val status = status
-// }
+ contentType(ContentType.Application.Json)
+ body = UpdateEntryRequest(entryIds, status)
}
override suspend fun toggleEntryBookmark(id: Long): HttpResponse =
diff --git a/network/src/main/java/com/wbrawner/nanoflux/network/repository/EntryRepository.kt b/network/src/main/java/com/wbrawner/nanoflux/network/repository/EntryRepository.kt
index 6cbf35f..1f376e5 100644
--- a/network/src/main/java/com/wbrawner/nanoflux/network/repository/EntryRepository.kt
+++ b/network/src/main/java/com/wbrawner/nanoflux/network/repository/EntryRepository.kt
@@ -1,9 +1,11 @@
package com.wbrawner.nanoflux.network.repository
+import com.wbrawner.nanoflux.network.EntryResponse
import com.wbrawner.nanoflux.network.MinifluxApiService
import com.wbrawner.nanoflux.storage.dao.EntryDao
import com.wbrawner.nanoflux.storage.model.Entry
import com.wbrawner.nanoflux.storage.model.EntryAndFeed
+import kotlinx.coroutines.flow.Flow
import timber.log.Timber
import javax.inject.Inject
@@ -12,15 +14,18 @@ class EntryRepository @Inject constructor(
private val entryDao: EntryDao,
private val logger: Timber.Tree
) {
- suspend fun getAll(fetch: Boolean = false): List {
+ fun observeUnread(): Flow> = entryDao.observeAll()
+
+ fun getCount(): Long = entryDao.getCount()
+
+ suspend fun getAll(fetch: Boolean = false, afterId: Long = 0): List {
if (fetch) {
- var page = 0
- while (true) {
- try {
- entryDao.insertAll(apiService.getEntries(offset = 1000L * page++, limit = 1000).entries)
- } catch (e: Exception) {
- break
- }
+ getEntries { page ->
+ apiService.getEntries(
+ offset = 100L * page,
+ limit = 100,
+ afterEntryId = afterId
+ )
}
}
return entryDao.getAll()
@@ -28,23 +33,47 @@ class EntryRepository @Inject constructor(
suspend fun getAllUnread(fetch: Boolean = false): List {
if (fetch) {
- var page = 0
- while (true) {
-// try {
- val response = apiService.getEntries(
- offset = 10000000000L,// * page++,
- limit = 1000,
- status = listOf(Entry.Status.UNREAD)
- )
- entryDao.insertAll(
- response.entries
- )
-// } catch (e: Exception) {
-// Log.e("EntryRepository", "Error", e)
-// break
-// }
+ getEntries { page ->
+ apiService.getEntries(
+ offset = 100L * page,
+ limit = 100,
+ status = listOf(Entry.Status.UNREAD)
+ )
}
}
return entryDao.getAllUnread()
}
+
+ suspend fun getEntry(id: Long): EntryAndFeed {
+ entryDao.getAllByIds(id).firstOrNull()?.let {
+ return@getEntry it
+ }
+
+ entryDao.insertAll(apiService.getEntry(id))
+ return entryDao.getAllByIds(id).first()
+ }
+
+ suspend fun markEntryRead(entry: Entry) {
+ apiService.updateEntries(listOf(entry.id), Entry.Status.READ)
+ entryDao.update(entry.copy(status = Entry.Status.READ))
+ }
+
+ suspend fun markEntryUnread(entry: Entry) {
+ apiService.updateEntries(listOf(entry.id), Entry.Status.UNREAD)
+ entryDao.update(entry.copy(status = Entry.Status.UNREAD))
+ }
+
+ fun getLatestId(): Long = entryDao.getLatestId()
+
+ private suspend fun getEntries(request: suspend (page: Int) -> EntryResponse) {
+ var page = 0
+ var totalPages = 1L
+ while (page++ < totalPages) {
+ val response = request(page)
+ entryDao.insertAll(
+ response.entries
+ )
+ totalPages = response.total / 100
+ }
+ }
}
\ No newline at end of file
diff --git a/network/src/main/java/com/wbrawner/nanoflux/network/repository/UserRepository.kt b/network/src/main/java/com/wbrawner/nanoflux/network/repository/UserRepository.kt
index fe1be70..1d6fb7a 100644
--- a/network/src/main/java/com/wbrawner/nanoflux/network/repository/UserRepository.kt
+++ b/network/src/main/java/com/wbrawner/nanoflux/network/repository/UserRepository.kt
@@ -38,7 +38,7 @@ class UserRepository @Inject constructor(
var correctedServer = if (server.startsWith("http")) {
server
} else {
- "http://$server"
+ "https://$server"
}
if (!correctedServer.endsWith("/v1")) {
correctedServer = if (correctedServer.endsWith("/")) {
diff --git a/storage/src/main/java/com/wbrawner/nanoflux/storage/dao/EntryDao.kt b/storage/src/main/java/com/wbrawner/nanoflux/storage/dao/EntryDao.kt
index d4cbc35..52ec07b 100644
--- a/storage/src/main/java/com/wbrawner/nanoflux/storage/dao/EntryDao.kt
+++ b/storage/src/main/java/com/wbrawner/nanoflux/storage/dao/EntryDao.kt
@@ -3,32 +3,49 @@ package com.wbrawner.nanoflux.storage.dao
import androidx.room.*
import com.wbrawner.nanoflux.storage.model.Entry
import com.wbrawner.nanoflux.storage.model.EntryAndFeed
+import kotlinx.coroutines.flow.Flow
@Dao
interface EntryDao {
+ @Query("SELECT COUNT(*) FROM Entry")
+ fun getCount(): Long
+
@Transaction
- @Query("SELECT * FROM Entry")
+ @Query("SELECT * FROM Entry ORDER BY createdAt DESC")
+ fun observeAll(): Flow>
+
+ @Transaction
+ @Query("SELECT * FROM Entry ORDER BY createdAt DESC")
fun getAll(): List
@Transaction
- @Query("SELECT * FROM Entry WHERE status = \"UNREAD\"")
+ @Query("SELECT * FROM Entry WHERE status = \"UNREAD\" ORDER BY createdAt DESC")
fun getAllUnread(): List
@Transaction
- @Query("SELECT * FROM Entry WHERE id in (:ids)")
+ @Query("SELECT * FROM Entry WHERE id in (:ids) ORDER BY createdAt DESC")
fun getAllByIds(vararg ids: Long): List
@Transaction
- @Query("SELECT * FROM Entry WHERE feedId in (:ids)")
+ @Query("SELECT * FROM Entry WHERE feedId in (:ids) ORDER BY createdAt DESC")
fun getAllByFeedIds(vararg ids: Long): List
@Transaction
- @Query("SELECT * FROM Entry WHERE status = \"UNREAD\" AND feedId in (:ids)")
+ @Query("SELECT * FROM Entry WHERE status = \"UNREAD\" AND feedId in (:ids) ORDER BY createdAt DESC")
fun getAllUnreadByFeedIds(vararg ids: Long): List
+ @Query("SELECT id FROM Entry ORDER BY id DESC LIMIT 1")
+ fun getLatestId(): Long
+
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(entries: List)
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertAll(vararg entry: Entry)
+
+ @Update
+ fun update(entry: Entry)
+
@Delete
fun delete(entry: Entry)
}
\ No newline at end of file