More progress on entries

This commit is contained in:
William Brawner 2021-06-10 21:19:49 -06:00
parent 7e243f1853
commit 2eba6c7c75
16 changed files with 398 additions and 101 deletions

View file

@ -14,6 +14,13 @@
<entry key="../../../../layout/compose-model-1622575673269.xml" value="0.2170608108108108" />
<entry key="../../../../layout/compose-model-1622578435598.xml" value="0.2170608108108108" />
<entry key="../../../../layout/compose-model-1622578490958.xml" value="0.2361111111111111" />
<entry key="../../../../layout/compose-model-1622633411023.xml" value="0.3091216216216216" />
<entry key="../../../../layout/compose-model-1622633755661.xml" value="0.4212962962962963" />
<entry key="../../../../layout/compose-model-1622646013817.xml" value="0.1638888888888889" />
<entry key="../../../../layout/compose-model-1622658640119.xml" value="0.2170608108108108" />
<entry key="../../../../layout/compose-model-1622768616366.xml" value="0.2179054054054054" />
<entry key="../../../../layout/compose-model-1622823049252.xml" value="2.0" />
<entry key="../../../../layout/compose-model-1623272040152.xml" value="0.6351851851851852" />
</map>
</option>
</component>

View file

@ -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)

View file

@ -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())

View file

@ -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())
}

View file

@ -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<String?>(null)
val errorMessage = _errorMessage.asStateFlow()
private val _entry = MutableStateFlow<EntryAndFeed?>(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}"
}

View file

@ -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>(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<EntryAndFeed>): FeedState()
}
}

View file

@ -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<String?>(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}"
}

View file

@ -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"

View file

@ -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)
}
},
)
}
}

View file

@ -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<SyncWorker>().build()
WorkManager.getInstance(context).enqueue(workRequest)
mainViewModel.loadUnread()
LaunchedEffect(key1 = context) {
WorkManager.getInstance(context)
.enqueue(OneTimeWorkRequestBuilder<SyncWorker>().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, {}, {}, {}, {}, {}, {})
}
}

View file

@ -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*/ }) {
}
}
}

View file

@ -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" }

View file

@ -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<Entry>)
@Serializable
data class UpdateEntryRequest(@SerialName("entry_ids") val entryIds: List<Long>, 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<String?> = 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<Long>, 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 =

View file

@ -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<EntryAndFeed> {
fun observeUnread(): Flow<List<EntryAndFeed>> = entryDao.observeAll()
fun getCount(): Long = entryDao.getCount()
suspend fun getAll(fetch: Boolean = false, afterId: Long = 0): List<EntryAndFeed> {
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<EntryAndFeed> {
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
}
}
}

View file

@ -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("/")) {

View file

@ -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<List<EntryAndFeed>>
@Transaction
@Query("SELECT * FROM Entry ORDER BY createdAt DESC")
fun getAll(): List<EntryAndFeed>
@Transaction
@Query("SELECT * FROM Entry WHERE status = \"UNREAD\"")
@Query("SELECT * FROM Entry WHERE status = \"UNREAD\" ORDER BY createdAt DESC")
fun getAllUnread(): List<EntryAndFeed>
@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<EntryAndFeed>
@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<EntryAndFeed>
@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<EntryAndFeed>
@Query("SELECT id FROM Entry ORDER BY id DESC LIMIT 1")
fun getLatestId(): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(entries: List<Entry>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg entry: Entry)
@Update
fun update(entry: Entry)
@Delete
fun delete(entry: Entry)
}