diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 2842237..d235beb 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -2,18 +2,23 @@
+
+
+
+
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 07ec910..72e1dad 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -26,6 +26,8 @@
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9924efa..d47581a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,8 +26,8 @@
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
index 6ad7052..d6b1d23 100644
--- a/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/UnreadViewModel.kt
+++ b/app/src/main/java/com/wbrawner/nanoflux/data/viewmodel/UnreadViewModel.kt
@@ -5,11 +5,13 @@ 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.*
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -19,7 +21,8 @@ class UnreadViewModel @Inject constructor(
private val feedRepository: FeedRepository,
private val categoryRepository: CategoryRepository,
private val entryRepository: EntryRepository,
- private val logger: Timber.Tree
+ private val iconRepository: IconRepository,
+ private val logger: Timber.Tree,
) : ViewModel() {
private val _loading = MutableStateFlow(false)
val loading = _loading.asStateFlow()
@@ -27,22 +30,35 @@ class UnreadViewModel @Inject constructor(
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}"
+ suspend fun share(entry: Entry): String? {
+ // Miniflux API doesn't currently support sharing so we have to send the external link for now
+ // https://github.com/miniflux/v2/issues/844
+// return withContext(Dispatchers.IO) {
+// entryRepository.share(entry)
+// }
+ return entry.url
+ }
fun refresh() = viewModelScope.launch(Dispatchers.IO) {
_loading.emit(true)
- feedRepository.getAll(fetch = true)
- categoryRepository.getAll(fetch = true)
- entryRepository.getAll(fetch = true)
+ syncAll(categoryRepository, feedRepository, iconRepository, entryRepository)
_loading.emit(false)
}
+
+ fun toggleRead(entry: Entry) = viewModelScope.launch(Dispatchers.IO) {
+ if (entry.status == Entry.Status.READ) {
+ entryRepository.markEntryUnread(entry)
+ } else if (entry.status == Entry.Status.UNREAD) {
+ entryRepository.markEntryRead(entry)
+ }
+ }
+
+ fun toggleStar(entry: Entry) = viewModelScope.launch(Dispatchers.IO) {
+ entryRepository.toggleStar(entry)
+ }
}
\ 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 0551935..d467371 100644
--- a/app/src/main/java/com/wbrawner/nanoflux/ui/Entries.kt
+++ b/app/src/main/java/com/wbrawner/nanoflux/ui/Entries.kt
@@ -1,8 +1,9 @@
package com.wbrawner.nanoflux.ui
-import android.content.Intent
import android.graphics.BitmapFactory
import android.util.Base64
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -10,14 +11,18 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Star
+import androidx.compose.material.icons.outlined.OpenInBrowser
import androidx.compose.material.icons.outlined.Star
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.swiperefresh.SwipeRefresh
@@ -27,6 +32,10 @@ import com.wbrawner.nanoflux.storage.model.*
import java.util.*
import kotlin.math.roundToInt
+@OptIn(
+ ExperimentalMaterialApi::class, ExperimentalAnimationApi::class,
+ ExperimentalFoundationApi::class
+)
@Composable
fun EntryList(
entries: List,
@@ -34,7 +43,8 @@ fun EntryList(
onFeedClicked: (feed: Feed) -> Unit,
onToggleReadClicked: (entry: Entry) -> Unit,
onStarClicked: (entry: Entry) -> Unit,
- onExternalLinkClicked: @Composable (entry: Entry) -> Unit,
+ onShareClicked: (entry: Entry) -> Unit,
+ onExternalLinkClicked: (entry: Entry) -> Unit,
isRefreshing: Boolean,
onRefresh: () -> Unit
) {
@@ -43,16 +53,58 @@ fun EntryList(
onRefresh = onRefresh
) {
LazyColumn {
- items(entries) { entry ->
- EntryListItem(
- entry.entry,
- entry.feed,
- onEntryItemClicked,
- onFeedClicked,
- onToggleReadClicked,
- onStarClicked,
- onExternalLinkClicked
- )
+ items(entries, { entry -> entry.entry.id }) { entry ->
+ val dismissState = rememberDismissState()
+ LaunchedEffect(key1 = dismissState.currentValue) {
+ when (dismissState.currentValue) {
+ DismissValue.DismissedToStart -> onToggleReadClicked(entry.entry)
+ DismissValue.DismissedToEnd -> {
+ onStarClicked(entry.entry)
+ dismissState.reset()
+ }
+ DismissValue.Default -> {}
+ }
+ }
+ SwipeToDismiss(
+ modifier = Modifier.animateItemPlacement(),
+ state = dismissState,
+ background = {
+ val (color, text, icon) = when (dismissState.dismissDirection) {
+ DismissDirection.StartToEnd ->
+ if (entry.entry.starred) {
+ Triple(Color.Yellow, "Unstarred", Icons.Outlined.Star)
+ } else {
+ Triple(Color.Yellow, "Starred", Icons.Filled.Star)
+ }
+ DismissDirection.EndToStart -> Triple(
+ Color.Green,
+ "Read",
+ Icons.Default.Email
+ )
+ else -> Triple(MaterialTheme.colors.surface, "", Icons.Default.Info)
+ }
+ Surface(modifier = Modifier.fillMaxSize(), color = color) {
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(imageVector = icon, contentDescription = null)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text)
+ }
+ }
+ }
+ ) {
+ EntryListItem(
+ entry.entry,
+ entry.feed,
+ onEntryItemClicked,
+ onFeedClicked = onFeedClicked,
+ onExternalLinkClicked = onExternalLinkClicked,
+ onShareClicked = onShareClicked,
+ )
+ }
}
}
}
@@ -64,85 +116,75 @@ fun EntryListItem(
feed: FeedCategoryIcon,
onEntryItemClicked: (entry: Entry) -> Unit,
onFeedClicked: (feed: Feed) -> Unit,
- onToggleReadClicked: (entry: Entry) -> Unit,
- onStarClicked: (entry: Entry) -> Unit,
- onExternalLinkClicked: @Composable (entry: Entry) -> Unit
+ onExternalLinkClicked: (entry: Entry) -> Unit,
+ onShareClicked: (entry: Entry) -> Unit
) {
-// val swipeState = rememberSwipeableState(initialValue = entry.status)
- Column(
+ Surface(
modifier = Modifier
- .padding(vertical = 8.dp, horizontal = 16.dp)
- .clickable { onEntryItemClicked(entry) }
-// .swipeable(state = swipeState, orientation = Orientation.Horizontal)
+ .clickable { onEntryItemClicked(entry) },
+ color = MaterialTheme.colors.surface
) {
- Row {
- Text(
- modifier = Modifier.weight(1f),
- text = entry.title,
- style = MaterialTheme.typography.h6
- )
- }
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
+ Column(
+ modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
) {
- TextButton(
- onClick = { onFeedClicked(feed.feed) },
- modifier = Modifier.padding(start = 0.dp),
- ) {
- feed.icon?.let {
- val bytes = Base64.decode(it.data.substringAfter(","), Base64.DEFAULT)
- val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
- ?.asImageBitmap()
- ?: return@let
- Image(
- modifier = Modifier
- .width(16.dp)
- .height(16.dp),
- bitmap = bitmap,
- contentDescription = null,
- )
- Spacer(modifier = Modifier.width(8.dp))
- }
+ Row {
Text(
- text = feed.feed.title,
- style = MaterialTheme.typography.body2,
- color = MaterialTheme.colors.onSurface
+ modifier = Modifier.weight(1f),
+ text = entry.title,
+ style = MaterialTheme.typography.h6
)
}
-// Text(text = feed.category.title)
- }
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier.fillMaxWidth()
- ) {
- Text(text = entry.publishedAt.timeSince(), style = MaterialTheme.typography.body2)
- Spacer(modifier = Modifier.width(8.dp))
- Text(text = "${entry.readingTime}m read", style = MaterialTheme.typography.body2)
- IconButton(onClick = { onStarClicked(entry) }) {
- Icon(
- imageVector = if (entry.starred) Icons.Filled.Star else Icons.Outlined.Star,
- contentDescription = if (entry.starred) "Starred" else "Not starred"
- )
- }
- val context = LocalContext.current
- IconButton(onClick = {
- val intent = Intent(Intent.ACTION_SEND).apply {
- // TODO: Get base url from viewmodel or something
- type = "text/plain"
- putExtra(
- Intent.EXTRA_TEXT,
- "https://wbrawner.com/entry/share/${entry.shareCode}"
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ TextButton(
+ onClick = { onFeedClicked(feed.feed) },
+ modifier = Modifier.padding(start = 0.dp),
+ ) {
+ feed.icon?.let {
+ val bytes = Base64.decode(it.data.substringAfter(","), Base64.DEFAULT)
+ val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
+ ?.asImageBitmap()
+ ?: return@let
+ Image(
+ modifier = Modifier
+ .width(16.dp)
+ .height(16.dp),
+ bitmap = bitmap,
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ }
+ Text(
+ text = feed.feed.title,
+ style = MaterialTheme.typography.body2,
+ color = MaterialTheme.colors.onSurface
+ )
+ }
+// Text(text = feed.category.title)
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(text = entry.publishedAt.timeSince(), style = MaterialTheme.typography.body2)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = "${entry.readingTime}m read", style = MaterialTheme.typography.body2)
+ IconButton(onClick = { onExternalLinkClicked(entry) }) {
+ Icon(
+ imageVector = Icons.Outlined.OpenInBrowser,
+ contentDescription = "Open in browser"
+ )
+ }
+ IconButton(onClick = { onShareClicked(entry) }) {
+ Icon(
+ imageVector = Icons.Default.Share,
+ contentDescription = "Share"
)
}
- context.startActivity(Intent.createChooser(intent, null))
- }) {
- Icon(
- imageVector = Icons.Default.Share,
- contentDescription = "Share"
- )
}
}
}
@@ -208,7 +250,7 @@ fun EntryListItem_Preview() {
mimeType = "image/png"
)
),
- {}, {}, {}, {}, {}
+ {}, {}, {}, {}
)
}
}
diff --git a/app/src/main/java/com/wbrawner/nanoflux/ui/EntryScreen.kt b/app/src/main/java/com/wbrawner/nanoflux/ui/EntryScreen.kt
index 32d1141..c979313 100644
--- a/app/src/main/java/com/wbrawner/nanoflux/ui/EntryScreen.kt
+++ b/app/src/main/java/com/wbrawner/nanoflux/ui/EntryScreen.kt
@@ -52,13 +52,14 @@ fun Entry(entry: EntryAndFeed) {
feed = entry.feed,
onEntryItemClicked = { /*TODO*/ },
onFeedClicked = { /*TODO*/ },
- onToggleReadClicked = { /*TODO*/ },
- onStarClicked = { /*TODO*/ },
- onExternalLinkClicked = { /* TODO */ }
+ onExternalLinkClicked = { /*TODO*/ },
+ onShareClicked = { /*TODO*/ },
)
// Adds view to Compose
AndroidView(
- modifier = Modifier.fillMaxSize().padding(16.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
factory = { context ->
TextView(context).apply {
text = Html.fromHtml(entry.entry.content)
diff --git a/app/src/main/java/com/wbrawner/nanoflux/ui/UnreadScreen.kt b/app/src/main/java/com/wbrawner/nanoflux/ui/UnreadScreen.kt
index fadf45e..8cf5fa8 100644
--- a/app/src/main/java/com/wbrawner/nanoflux/ui/UnreadScreen.kt
+++ b/app/src/main/java/com/wbrawner/nanoflux/ui/UnreadScreen.kt
@@ -2,8 +2,12 @@ package com.wbrawner.nanoflux.ui
import android.content.Intent
import android.net.Uri
-import androidx.compose.material.*
-import androidx.compose.runtime.*
+import androidx.compose.material.SnackbarHostState
+import androidx.compose.material.SnackbarResult
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
@@ -28,6 +32,7 @@ fun UnreadScreen(
}
}
}
+ val context = LocalContext.current
EntryList(
entries = entries,
onEntryItemClicked = {
@@ -36,10 +41,24 @@ fun UnreadScreen(
onFeedClicked = {
navController.navigate("feeds/${it.id}")
},
- onToggleReadClicked = { /*TODO*/ },
- onStarClicked = { /*TODO*/ },
+ onToggleReadClicked = { unreadViewModel.toggleRead(it) },
+ onStarClicked = { unreadViewModel.toggleStar(it) },
onExternalLinkClicked = {
- LocalContext.current.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url)))
+ context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url)))
+ },
+ onShareClicked = { entry ->
+ coroutineScope.launch {
+ unreadViewModel.share(entry)?.let { url ->
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(
+ Intent.EXTRA_TEXT,
+ url
+ )
+ }
+ context.startActivity(Intent.createChooser(intent, null))
+ }
+ }
},
isRefreshing = loading,
onRefresh = {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ef07eed..19dfbc0 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -18,13 +18,14 @@ versionName = "1.0"
work = "2.7.1"
[libraries]
-accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version = "0.26.3-beta"}
+accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version = "0.26.3-beta" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
material = { module = "com.google.android.material:material", version.ref = "material" }
compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose-compiler" }
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" }
compose-material = { module = "androidx.compose.material:material", version.ref = "compose-ui" }
+compose-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose-ui" }
compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-ui" }
compose-activity = { module = "androidx.activity:activity-compose", version = "1.5.1" }
compose-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-ui" }
@@ -42,13 +43,13 @@ junit = { module = "junit:junit", version = "4.12" }
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlin-reflection = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
-kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.4.0"}
-ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor"}
-ktor-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor"}
+kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.4.0" }
+ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
+ktor-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
-ktor-logging = { module = "io.ktor:ktor-client-logging-jvm", version.ref = "ktor"}
-ktor-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor"}
+ktor-logging = { module = "io.ktor:ktor-client-logging-jvm", version.ref = "ktor" }
+ktor-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-kapt = { module = "androidx.room:room-compiler", version.ref = "room" }
room-test = { module = "androidx.room:room-testing", version.ref = "room" }
@@ -59,7 +60,7 @@ work-core = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
work-test = { module = "androidx.work:work-testing", version.ref = "work" }
[bundles]
-compose = ["compose-compiler", "compose-ui", "compose-material", "compose-tooling", "compose-activity", "navigation-compose"]
+compose = ["compose-compiler", "compose-ui", "compose-material", "compose-icons", "compose-tooling", "compose-activity", "navigation-compose"]
coroutines = ["coroutines-core", "coroutines-android"]
networking = ["kotlin-reflection", "kotlinx-serialization", "ktor-core", "ktor-cio", "ktor-content-negotiation", "ktor-json", "ktor-logging", "ktor-serialization"]
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 297e209..ac909c5 100644
--- a/network/src/main/java/com/wbrawner/nanoflux/network/MinifluxApiService.kt
+++ b/network/src/main/java/com/wbrawner/nanoflux/network/MinifluxApiService.kt
@@ -1,6 +1,7 @@
package com.wbrawner.nanoflux.network
import android.content.SharedPreferences
+import androidx.core.content.edit
import com.wbrawner.nanoflux.network.repository.PREF_KEY_AUTH_TOKEN
import com.wbrawner.nanoflux.storage.model.*
import io.ktor.client.*
@@ -22,7 +23,7 @@ import javax.inject.Inject
interface MinifluxApiService {
- fun setBaseUrl(url: String)
+ var baseUrl: String
suspend fun discoverSubscriptions(
url: String,
@@ -91,6 +92,8 @@ interface MinifluxApiService {
suspend fun toggleEntryBookmark(id: Long): HttpResponse
+ suspend fun shareEntry(entry: Entry): HttpResponse
+
suspend fun getCategories(): List
suspend fun createCategory(title: String): Category
@@ -169,7 +172,10 @@ data class CreateFeedResponse(@SerialName("feed_id") val feedId: Long)
data class EntryResponse(val total: Long, val entries: List)
@Serializable
-data class UpdateEntryRequest(@SerialName("entry_ids") val entryIds: List, val status: Entry.Status)
+data class UpdateEntryRequest(
+ @SerialName("entry_ids") val entryIds: List,
+ val status: Entry.Status
+)
@Suppress("unused")
class KtorMinifluxApiService @Inject constructor(
@@ -194,13 +200,19 @@ class KtorMinifluxApiService @Inject constructor(
}
}
private val _baseUrl: AtomicReference = AtomicReference()
- private val baseUrl: String
+ override var baseUrl: String
get() = _baseUrl.get() ?: run {
sharedPreferences.getString(PREF_KEY_BASE_URL, null)?.let {
_baseUrl.set(it)
it
} ?: ""
}
+ set(value) {
+ _baseUrl.set(value)
+ sharedPreferences.edit {
+ putString(PREF_KEY_BASE_URL, value)
+ }
+ }
private fun url(
path: String,
@@ -220,10 +232,6 @@ class KtorMinifluxApiService @Inject constructor(
return url.build()
}
- override fun setBaseUrl(url: String) {
- _baseUrl.set(url)
- }
-
override suspend fun discoverSubscriptions(
url: String,
username: String?,
@@ -345,8 +353,8 @@ class KtorMinifluxApiService @Inject constructor(
"status" to status?.joinToString(",") { it.name.toLowerCase() },
"offset" to offset,
"limit" to limit,
- "order" to order,
- "direction" to direction,
+ "order" to order?.name?.lowercase(),
+ "direction" to direction?.name?.lowercase(),
"before" to before,
"after" to after,
"before_entry_id" to beforeEntryId,
@@ -375,6 +383,23 @@ class KtorMinifluxApiService @Inject constructor(
override suspend fun toggleEntryBookmark(id: Long): HttpResponse =
client.put(url("entries/$id/bookmark"))
+ override suspend fun shareEntry(entry: Entry): HttpResponse {
+ // This is the only method that doesn't really go through the API because there doesn't
+ // appear to be a documented way of getting the share code, so we have to build the URL
+ // manually
+ val url = URLBuilder(baseUrl).apply {
+ path("entry", "share", entry.id.toString())
+ }.build()
+ return client.get(url) {
+ headers {
+ header(
+ "Authorization",
+ sharedPreferences.getString(PREF_KEY_AUTH_TOKEN, "").toString()
+ )
+ }
+ }
+ }
+
override suspend fun getCategories(): List = client.get(url("categories")) {
headers {
header("Authorization", sharedPreferences.getString(PREF_KEY_AUTH_TOKEN, "").toString())
diff --git a/network/src/main/java/com/wbrawner/nanoflux/network/repository/CategoryRepository.kt b/network/src/main/java/com/wbrawner/nanoflux/network/repository/CategoryRepository.kt
index accfbb1..cc07e23 100644
--- a/network/src/main/java/com/wbrawner/nanoflux/network/repository/CategoryRepository.kt
+++ b/network/src/main/java/com/wbrawner/nanoflux/network/repository/CategoryRepository.kt
@@ -5,7 +5,9 @@ import com.wbrawner.nanoflux.storage.dao.CategoryDao
import com.wbrawner.nanoflux.storage.model.Category
import timber.log.Timber
import javax.inject.Inject
+import javax.inject.Singleton
+@Singleton
class CategoryRepository @Inject constructor(
private val apiService: MinifluxApiService,
private val categoryDao: CategoryDao,
@@ -20,6 +22,6 @@ class CategoryRepository @Inject constructor(
}
suspend fun getById(id: Long): Category? = categoryDao.getAllByIds(id).firstOrNull()
- ?: getAll(true)
+ ?: getAll(true)
.firstOrNull { it.id == id }
}
\ No newline at end of file
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 b750f75..5e0b7bb 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,14 +1,20 @@
package com.wbrawner.nanoflux.network.repository
+import android.net.Uri
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 io.ktor.http.*
+import io.ktor.util.network.*
import kotlinx.coroutines.flow.Flow
import timber.log.Timber
+import java.io.IOException
import javax.inject.Inject
+import javax.inject.Singleton
+@Singleton
class EntryRepository @Inject constructor(
private val apiService: MinifluxApiService,
private val entryDao: EntryDao,
@@ -22,6 +28,8 @@ class EntryRepository @Inject constructor(
if (fetch) {
getEntries { page ->
apiService.getEntries(
+ order = Entry.Order.PUBLISHED_AT,
+ direction = Entry.SortDirection.DESC,
offset = 100L * page,
limit = 100,
afterEntryId = afterId
@@ -54,13 +62,37 @@ class EntryRepository @Inject constructor(
}
suspend fun markEntryRead(entry: Entry) {
- apiService.updateEntries(listOf(entry.id), Entry.Status.READ)
entryDao.update(entry.copy(status = Entry.Status.READ))
+ retryOnFailure {
+ apiService.updateEntries(listOf(entry.id), Entry.Status.READ)
+ }
}
suspend fun markEntryUnread(entry: Entry) {
- apiService.updateEntries(listOf(entry.id), Entry.Status.UNREAD)
entryDao.update(entry.copy(status = Entry.Status.UNREAD))
+ retryOnFailure {
+ apiService.updateEntries(listOf(entry.id), Entry.Status.UNREAD)
+ }
+ }
+
+ suspend fun toggleStar(entry: Entry) {
+ entryDao.update(entry.copy(starred = !entry.starred))
+ retryOnFailure {
+ apiService.toggleEntryBookmark(entry.id)
+ }
+ }
+
+ suspend fun share(entry: Entry): String? {
+ if (entry.shareCode.isNotBlank()) {
+ return Uri.parse(apiService.baseUrl)
+ .buildUpon()
+ .path("/entry/share/${entry.shareCode}")
+ .build()
+ .toString()
+ }
+
+ val response = apiService.shareEntry(entry)
+ return response.headers[HttpHeaders.Location]
}
fun getLatestId(): Long = entryDao.getLatestId()
@@ -76,6 +108,22 @@ class EntryRepository @Inject constructor(
totalPages = response.total / 100
}
}
+
+
+ private suspend fun retryOnFailure(request: suspend () -> Any) {
+ try {
+ request()
+ } catch (e: Exception) {
+ when (e) {
+ is IOException, is UnresolvedAddressException -> {
+ // TODO: Save request to retry later
+ logger.e("Network request failed", e)
+ }
+ // TODO: Log to Crashlytics or something instead of just crashing
+ else -> throw RuntimeException("Unhandled exception marking entry as read", e)
+ }
+ }
+ }
}
enum class EntryStatus {
diff --git a/network/src/main/java/com/wbrawner/nanoflux/network/repository/FeedRepository.kt b/network/src/main/java/com/wbrawner/nanoflux/network/repository/FeedRepository.kt
index e61f626..3314fa8 100644
--- a/network/src/main/java/com/wbrawner/nanoflux/network/repository/FeedRepository.kt
+++ b/network/src/main/java/com/wbrawner/nanoflux/network/repository/FeedRepository.kt
@@ -5,7 +5,9 @@ import com.wbrawner.nanoflux.storage.dao.FeedDao
import com.wbrawner.nanoflux.storage.model.FeedCategoryIcon
import timber.log.Timber
import javax.inject.Inject
+import javax.inject.Singleton
+@Singleton
class FeedRepository @Inject constructor(
private val apiService: MinifluxApiService,
private val feedDao: FeedDao,
diff --git a/network/src/main/java/com/wbrawner/nanoflux/network/repository/IconRepository.kt b/network/src/main/java/com/wbrawner/nanoflux/network/repository/IconRepository.kt
index ce62cb1..d50f0ab 100644
--- a/network/src/main/java/com/wbrawner/nanoflux/network/repository/IconRepository.kt
+++ b/network/src/main/java/com/wbrawner/nanoflux/network/repository/IconRepository.kt
@@ -5,7 +5,9 @@ import com.wbrawner.nanoflux.storage.dao.IconDao
import com.wbrawner.nanoflux.storage.model.Feed
import timber.log.Timber
import javax.inject.Inject
+import javax.inject.Singleton
+@Singleton
class IconRepository @Inject constructor(
private val apiService: MinifluxApiService,
private val iconDao: IconDao,
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 1d6fb7a..5254f19 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
@@ -12,10 +12,12 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
+import javax.inject.Singleton
const val PREF_KEY_AUTH_TOKEN = "authToken"
const val PREF_KEY_CURRENT_USER = "currentUser"
+@Singleton
class UserRepository @Inject constructor(
private val sharedPreferences: SharedPreferences,
private val apiService: MinifluxApiService,
@@ -52,9 +54,8 @@ class UserRepository @Inject constructor(
Base64.DEFAULT or Base64.NO_WRAP
)
.decodeToString()
- apiService.setBaseUrl(correctedServer)
+ apiService.baseUrl = correctedServer
sharedPreferences.edit {
- putString(PREF_KEY_BASE_URL, correctedServer)
putString(PREF_KEY_AUTH_TOKEN, "Basic $credentials")
}
try {
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 fd8c19e..9e71046 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
@@ -18,6 +18,10 @@ interface EntryDao {
@Query("SELECT * FROM Entry WHERE status = \"UNREAD\" ORDER BY publishedAt DESC")
fun observeUnread(): Flow>
+ @Transaction
+ @Query("SELECT * FROM Entry WHERE status = \"READ\" ORDER BY publishedAt DESC")
+ fun observeRead(): Flow>
+
@Transaction
@Query("SELECT * FROM Entry ORDER BY publishedAt DESC")
fun getAll(): List