mirror of https://github.com/readrops/Readrops.git
Add OPML import in AccountTab
This commit is contained in:
parent
1a92684c18
commit
e0874f2297
|
@ -78,4 +78,6 @@ dependencies {
|
|||
|
||||
debugApi 'com.chimerapps.niddler:niddler:1.5.5'
|
||||
releaseApi 'com.chimerapps.niddler:niddler-noop:1.5.5'
|
||||
|
||||
testImplementation(libs.coroutines.test)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package com.readrops.api.opml
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import org.redundent.kotlin.xml.xml
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
@ -12,48 +11,37 @@ import java.io.OutputStream
|
|||
object OPMLParser {
|
||||
|
||||
@JvmStatic
|
||||
fun read(stream: InputStream): Single<Map<Folder?, List<Feed>>> {
|
||||
return Single.create { emitter ->
|
||||
try {
|
||||
val adapter = OPMLAdapter()
|
||||
val opml = adapter.fromXml(stream.konsumeXml())
|
||||
suspend fun read(stream: InputStream): Map<Folder?, List<Feed>> {
|
||||
try {
|
||||
val adapter = OPMLAdapter()
|
||||
val opml = adapter.fromXml(stream.konsumeXml())
|
||||
|
||||
emitter.onSuccess(opml)
|
||||
} catch (e: Exception) {
|
||||
emitter.onError(e)
|
||||
}
|
||||
stream.close()
|
||||
return opml
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun write(foldersAndFeeds: Map<Folder?, List<Feed>>, outputStream: OutputStream): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val opml = xml("opml") {
|
||||
attribute("version", "2.0")
|
||||
suspend fun write(foldersAndFeeds: Map<Folder?, List<Feed>>, outputStream: OutputStream) {
|
||||
val opml = xml("opml") {
|
||||
attribute("version", "2.0")
|
||||
|
||||
"head" {
|
||||
-"Subscriptions"
|
||||
}
|
||||
"head" {
|
||||
-"Subscriptions"
|
||||
}
|
||||
|
||||
"body" {
|
||||
for (folderAndFeeds in foldersAndFeeds) {
|
||||
if (folderAndFeeds.key != null) { // feeds with folder
|
||||
"outline" {
|
||||
folderAndFeeds.key?.name?.let {
|
||||
attribute("title", it)
|
||||
attribute("text", it)
|
||||
}
|
||||
|
||||
for (feed in folderAndFeeds.value) {
|
||||
"outline" {
|
||||
feed.name?.let { attribute("title", it) }
|
||||
attribute("xmlUrl", feed.url!!)
|
||||
feed.siteUrl?.let { attribute("htmlUrl", it) }
|
||||
}
|
||||
}
|
||||
"body" {
|
||||
for (folderAndFeeds in foldersAndFeeds) {
|
||||
if (folderAndFeeds.key != null) { // feeds with folder
|
||||
"outline" {
|
||||
folderAndFeeds.key?.name?.let {
|
||||
attribute("title", it)
|
||||
attribute("text", it)
|
||||
}
|
||||
} else {
|
||||
for (feed in folderAndFeeds.value) { // feeds without folder
|
||||
|
||||
for (feed in folderAndFeeds.value) {
|
||||
"outline" {
|
||||
feed.name?.let { attribute("title", it) }
|
||||
attribute("xmlUrl", feed.url!!)
|
||||
|
@ -61,14 +49,20 @@ object OPMLParser {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (feed in folderAndFeeds.value) { // feeds without folder
|
||||
"outline" {
|
||||
feed.name?.let { attribute("title", it) }
|
||||
attribute("xmlUrl", feed.url!!)
|
||||
feed.siteUrl?.let { attribute("htmlUrl", it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.write(opml.toString().toByteArray())
|
||||
outputStream.flush()
|
||||
|
||||
emitter.onComplete()
|
||||
}
|
||||
|
||||
outputStream.write(opml.toString().toByteArray())
|
||||
outputStream.flush()
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@ object ApiUtils {
|
|||
const val HTTP_NOT_FOUND = 404
|
||||
const val HTTP_CONFLICT = 409
|
||||
|
||||
val OPML_MIMETYPES = listOf("application/xml", "text/xml", "text/x-opml")
|
||||
|
||||
private const val RSS_CONTENT_TYPE_REGEX = "([^;]+)"
|
||||
|
||||
fun isMimeImage(type: String): Boolean =
|
||||
|
|
|
@ -4,8 +4,8 @@ import com.readrops.api.TestUtils
|
|||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
@ -13,83 +13,85 @@ import java.io.FileOutputStream
|
|||
class OPMLParserTest {
|
||||
|
||||
@Test
|
||||
fun readOpmlTest() {
|
||||
fun readOpmlTest() = runTest {
|
||||
val stream = TestUtils.loadResource("opml/subscriptions.opml")
|
||||
val foldersAndFeeds = OPMLParser.read(stream)
|
||||
|
||||
var foldersAndFeeds: Map<Folder?, List<Feed>>? = null
|
||||
assertEquals(foldersAndFeeds.size, 6)
|
||||
|
||||
OPMLParser.read(stream)
|
||||
.observeOn(Schedulers.trampoline())
|
||||
.subscribeOn(Schedulers.trampoline())
|
||||
.subscribe { result -> foldersAndFeeds = result }
|
||||
|
||||
assertEquals(foldersAndFeeds?.size, 6)
|
||||
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Folder 1"))?.size, 2)
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Subfolder 1"))?.size, 4)
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Subfolder 2"))?.size, 1)
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Sub subfolder 1"))?.size, 2)
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Sub subfolder 2"))?.size, 0)
|
||||
assertEquals(foldersAndFeeds?.get(null)?.size, 2)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Folder 1")]?.size, 2)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Subfolder 1")]?.size, 4)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Subfolder 2")]?.size, 1)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Sub subfolder 1")]?.size, 2)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Sub subfolder 2")]?.size, 0)
|
||||
assertEquals(foldersAndFeeds[null]?.size, 2)
|
||||
|
||||
stream.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readLiteSubscriptionsTest() {
|
||||
fun readLiteSubscriptionsTest() = runTest {
|
||||
val stream = TestUtils.loadResource("opml/lite_subscriptions.opml")
|
||||
|
||||
var foldersAndFeeds: Map<Folder?, List<Feed>>? = null
|
||||
val foldersAndFeeds = OPMLParser.read(stream)
|
||||
|
||||
OPMLParser.read(stream)
|
||||
.subscribe { result -> foldersAndFeeds = result }
|
||||
|
||||
assertEquals(foldersAndFeeds?.values?.first()?.size, 2)
|
||||
assertEquals(foldersAndFeeds?.values?.first()?.first()?.url, "http://www.theverge.com/rss/index.xml")
|
||||
assertEquals(foldersAndFeeds?.values?.first()?.get(1)?.url, "https://techcrunch.com/feed/")
|
||||
assertEquals(foldersAndFeeds.values.first().size, 2)
|
||||
assertEquals(
|
||||
foldersAndFeeds.values.first().first().url,
|
||||
"http://www.theverge.com/rss/index.xml"
|
||||
)
|
||||
assertEquals(foldersAndFeeds.values.first()[1].url, "https://techcrunch.com/feed/")
|
||||
|
||||
stream.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun opmlVersionTest() {
|
||||
@Test(expected = ParseException::class)
|
||||
fun opmlVersionTest() = runTest {
|
||||
val stream = TestUtils.loadResource("opml/wrong_version.opml")
|
||||
|
||||
OPMLParser.read(stream)
|
||||
.test()
|
||||
.assertError(ParseException::class.java)
|
||||
|
||||
stream.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOpmlTest() {
|
||||
fun writeOpmlTest() = runTest {
|
||||
val file = File("subscriptions.opml")
|
||||
val outputStream = FileOutputStream(file)
|
||||
|
||||
val foldersAndFeeds: Map<Folder?, List<Feed>> = HashMap<Folder?, List<Feed>>().apply {
|
||||
put(null, listOf(Feed(name = "Feed1", url = "https://feed1.com"),
|
||||
Feed(name = "Feed2", url = "https://feed2.com")))
|
||||
put(
|
||||
null, listOf(
|
||||
Feed(name = "Feed1", url = "https://feed1.com"),
|
||||
Feed(name = "Feed2", url = "https://feed2.com")
|
||||
)
|
||||
)
|
||||
put(Folder(name = "Folder1"), listOf())
|
||||
put(Folder(name = "Folder2"), listOf(Feed(name = "Feed3", url = "https://feed3.com"),
|
||||
Feed(name = "Feed4", url ="https://feed4.com")))
|
||||
put(
|
||||
Folder(name = "Folder2"), listOf(
|
||||
Feed(name = "Feed3", url = "https://feed3.com"),
|
||||
Feed(name = "Feed4", url = "https://feed4.com")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
OPMLParser.write(foldersAndFeeds, outputStream)
|
||||
.subscribeOn(Schedulers.trampoline())
|
||||
.subscribe()
|
||||
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
|
||||
val inputStream = file.inputStream()
|
||||
var foldersAndFeeds2: Map<Folder?, List<Feed>>? = null
|
||||
OPMLParser.read(inputStream).subscribe { result -> foldersAndFeeds2 = result }
|
||||
val foldersAndFeeds2 = OPMLParser.read(inputStream)
|
||||
|
||||
assertEquals(foldersAndFeeds.size, foldersAndFeeds2?.size)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Folder1")]?.size, foldersAndFeeds2?.get(Folder(name = "Folder1"))?.size)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Folder2")]?.size, foldersAndFeeds2?.get(Folder(name = "Folder2"))?.size)
|
||||
assertEquals(foldersAndFeeds[null]?.size, foldersAndFeeds2?.get(null)?.size)
|
||||
assertEquals(foldersAndFeeds.size, foldersAndFeeds2.size)
|
||||
assertEquals(
|
||||
foldersAndFeeds[Folder(name = "Folder1")]?.size,
|
||||
foldersAndFeeds2[Folder(name = "Folder1")]?.size
|
||||
)
|
||||
assertEquals(
|
||||
foldersAndFeeds[Folder(name = "Folder2")]?.size,
|
||||
foldersAndFeeds2[Folder(name = "Folder2")]?.size
|
||||
)
|
||||
assertEquals(foldersAndFeeds[null]?.size, foldersAndFeeds2[null]?.size)
|
||||
|
||||
inputStream.close()
|
||||
}
|
||||
|
|
|
@ -64,7 +64,8 @@ public class AccountViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
public Completable parseOPMLFile(Uri uri, Context context) throws FileNotFoundException {
|
||||
return OPMLParser.read(context.getContentResolver().openInputStream(uri))
|
||||
.flatMapCompletable(foldersAndFeeds -> repository.insertOPMLFoldersAndFeeds(foldersAndFeeds));
|
||||
/*return OPMLParser.read(context.getContentResolver().openInputStream(uri))
|
||||
.flatMapCompletable(foldersAndFeeds -> repository.insertOPMLFoldersAndFeeds(foldersAndFeeds));*/
|
||||
return Completable.complete();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -257,8 +257,8 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
|||
.blockingGet();
|
||||
|
||||
|
||||
OPMLParser.write(folderListMap, outputStream)
|
||||
.blockingAwait();
|
||||
/*OPMLParser.write(folderListMap, outputStream)
|
||||
.blockingAwait();*/
|
||||
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.readrops.app.compose.account
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.readrops.api.opml.OPMLParser
|
||||
import com.readrops.app.compose.base.TabScreenModel
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.account.Account
|
||||
|
@ -45,6 +48,39 @@ class AccountScreenModel(
|
|||
_closeHome.update { true }
|
||||
}
|
||||
}
|
||||
|
||||
fun parseOPMLFile(uri: Uri, context: Context) {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
val stream = context.contentResolver.openInputStream(uri)!!
|
||||
val foldersAndFeeds = OPMLParser.read(stream)
|
||||
|
||||
openDialog(
|
||||
DialogState.OPMLImport(
|
||||
currentFeed = foldersAndFeeds.values.first().first().name!!,
|
||||
feedCount = 0,
|
||||
feedMax = foldersAndFeeds.values.flatten().size
|
||||
)
|
||||
)
|
||||
|
||||
repository?.insertOPMLFoldersAndFeeds(
|
||||
foldersAndFeeds = foldersAndFeeds,
|
||||
onUpdate = { feed ->
|
||||
_accountState.update {
|
||||
val dialog = (it.dialog as DialogState.OPMLImport)
|
||||
|
||||
it.copy(
|
||||
dialog = dialog.copy(
|
||||
currentFeed = feed.name!!,
|
||||
feedCount = dialog.feedCount + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AccountState(
|
||||
|
@ -55,4 +91,6 @@ data class AccountState(
|
|||
sealed interface DialogState {
|
||||
object DeleteAccount : DialogState
|
||||
object NewAccount : DialogState
|
||||
data class OPMLImport(val currentFeed: String, val feedCount: Int, val feedMax: Int) :
|
||||
DialogState
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package com.readrops.app.compose.account
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -24,6 +26,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -33,6 +36,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import cafe.adriel.voyager.navigator.tab.Tab
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import com.readrops.api.utils.ApiUtils
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.account.credentials.AccountCredentialsScreen
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionDialog
|
||||
|
@ -56,6 +60,7 @@ object AccountTab : Tab {
|
|||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val viewModel = getScreenModel<AccountScreenModel>()
|
||||
|
||||
val closeHome by viewModel.closeHome.collectAsStateWithLifecycle()
|
||||
|
@ -65,8 +70,13 @@ object AccountTab : Tab {
|
|||
navigator.replaceAll(AccountSelectionScreen())
|
||||
}
|
||||
|
||||
when (state.dialog) {
|
||||
DialogState.DeleteAccount -> {
|
||||
val launcher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
uri?.let { viewModel.parseOPMLFile(uri, context) }
|
||||
}
|
||||
|
||||
when (val dialog = state.dialog) {
|
||||
is DialogState.DeleteAccount -> {
|
||||
TwoChoicesDialog(
|
||||
title = stringResource(R.string.delete_account),
|
||||
text = stringResource(R.string.delete_account_question),
|
||||
|
@ -81,7 +91,7 @@ object AccountTab : Tab {
|
|||
)
|
||||
}
|
||||
|
||||
DialogState.NewAccount -> {
|
||||
is DialogState.NewAccount -> {
|
||||
AccountSelectionDialog(
|
||||
onDismiss = { viewModel.closeDialog() },
|
||||
onValidate = { accountType ->
|
||||
|
@ -91,6 +101,14 @@ object AccountTab : Tab {
|
|||
)
|
||||
}
|
||||
|
||||
is DialogState.OPMLImport -> {
|
||||
OPMLImportProgressDialog(
|
||||
currentFeed = dialog.currentFeed,
|
||||
feedCount = dialog.feedCount,
|
||||
feedMax = dialog.feedMax
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
|
@ -164,6 +182,15 @@ object AccountTab : Tab {
|
|||
onClick = { }
|
||||
)
|
||||
|
||||
SelectableIconText(
|
||||
icon = painterResource(id = R.drawable.ic_import_export),
|
||||
text = stringResource(R.string.opml_import_export),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
spacing = MaterialTheme.spacing.mediumSpacing,
|
||||
padding = MaterialTheme.spacing.mediumSpacing,
|
||||
onClick = { launcher.launch(ApiUtils.OPML_MIMETYPES.toTypedArray()) }
|
||||
)
|
||||
|
||||
SelectableIconText(
|
||||
icon = rememberVectorPainter(image = Icons.Default.AccountCircle),
|
||||
text = stringResource(R.string.delete_account),
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package com.readrops.app.compose.account
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.util.components.BaseDialog
|
||||
import com.readrops.app.compose.util.components.RefreshIndicator
|
||||
|
||||
@Composable
|
||||
fun OPMLImportProgressDialog(
|
||||
currentFeed: String,
|
||||
feedCount: Int,
|
||||
feedMax: Int,
|
||||
) {
|
||||
BaseDialog(
|
||||
title = stringResource(id = R.string.opml_import),
|
||||
icon = painterResource(R.drawable.ic_import_export),
|
||||
onDismiss = {}
|
||||
) {
|
||||
RefreshIndicator(
|
||||
currentFeed = currentFeed,
|
||||
feedCount = feedCount,
|
||||
feedMax = feedMax
|
||||
)
|
||||
}
|
||||
}
|
|
@ -188,7 +188,9 @@ class FeedScreenModel(
|
|||
try {
|
||||
if (localRSSDataSource.isUrlRSSResource(url)) {
|
||||
// TODO add support for all account types
|
||||
repository?.insertNewFeeds(listOf(url))
|
||||
repository?.insertNewFeeds(
|
||||
newFeeds = listOf(Feed(url = url))
|
||||
) {}
|
||||
|
||||
closeDialog(DialogState.AddFeed)
|
||||
} else {
|
||||
|
@ -200,7 +202,9 @@ class FeedScreenModel(
|
|||
}
|
||||
} else {
|
||||
// TODO add support for all account types
|
||||
repository?.insertNewFeeds(rssUrls.map { it.url })
|
||||
repository?.insertNewFeeds(
|
||||
newFeeds = rssUrls.map { Feed(url = it.url) }
|
||||
) {}
|
||||
|
||||
closeDialog(DialogState.AddFeed)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ abstract class ARepository(
|
|||
*/
|
||||
abstract suspend fun synchronize(): SyncResult
|
||||
|
||||
abstract suspend fun insertNewFeeds(urls: List<String>)
|
||||
abstract suspend fun insertNewFeeds(newFeeds: List<Feed>, onUpdate: (Feed) -> Unit)
|
||||
}
|
||||
|
||||
abstract class BaseRepository(
|
||||
|
@ -45,7 +45,8 @@ abstract class BaseRepository(
|
|||
account: Account,
|
||||
) : ARepository(database, account) {
|
||||
|
||||
open suspend fun updateFeed(feed: Feed) = database.newFeedDao().updateFeedFields(feed.id, feed.name!!, feed.url!!, feed.folderId)
|
||||
open suspend fun updateFeed(feed: Feed) =
|
||||
database.newFeedDao().updateFeedFields(feed.id, feed.name!!, feed.url!!, feed.folderId)
|
||||
|
||||
open suspend fun deleteFeed(feed: Feed) = database.newFeedDao().delete(feed)
|
||||
|
||||
|
@ -82,4 +83,30 @@ abstract class BaseRepository(
|
|||
open suspend fun setAllItemsReadByFolder(folderId: Int, accountId: Int) {
|
||||
database.newItemDao().setAllItemsReadByFolder(folderId, accountId)
|
||||
}
|
||||
|
||||
suspend fun insertOPMLFoldersAndFeeds(
|
||||
foldersAndFeeds: Map<Folder?, List<Feed>>,
|
||||
onUpdate: (Feed) -> Unit
|
||||
) {
|
||||
for ((folder, feeds) in foldersAndFeeds) {
|
||||
if (folder != null) {
|
||||
folder.accountId = account.id
|
||||
|
||||
val dbFolder = database.newFolderDao().selectFolderByName(folder.name!!, account.id)
|
||||
|
||||
if (dbFolder != null) {
|
||||
folder.id = dbFolder.id
|
||||
} else {
|
||||
folder.id = database.newFolderDao().insert(folder).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
feeds.forEach { it.folderId = folder?.id }
|
||||
|
||||
insertNewFeeds(
|
||||
newFeeds = feeds,
|
||||
onUpdate = onUpdate
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.readrops.app.compose.repositories
|
||||
|
||||
import android.util.Log
|
||||
import com.readrops.api.localfeed.LocalRSSDataSource
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.api.utils.ApiUtils
|
||||
|
@ -68,16 +69,19 @@ class LocalRSSRepository(
|
|||
throw NotImplementedError("This method can't be called here")
|
||||
|
||||
|
||||
override suspend fun insertNewFeeds(urls: List<String>) = withContext(Dispatchers.IO) {
|
||||
for (url in urls) {
|
||||
try {
|
||||
val result = dataSource.queryRSSResource(url, null)!!
|
||||
insertFeed(result.first)
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
override suspend fun insertNewFeeds(newFeeds: List<Feed>, onUpdate: (Feed) -> Unit) = withContext(Dispatchers.IO) {
|
||||
for (newFeed in newFeeds) {
|
||||
onUpdate(newFeed)
|
||||
|
||||
try {
|
||||
val result = dataSource.queryRSSResource(newFeed.url!!, null)!!
|
||||
insertFeed(result.first.also { it.folderId = newFeed.folderId })
|
||||
} catch (e: Exception) {
|
||||
Log.d("LocalRSSRepository", e.message.orEmpty())
|
||||
//throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun insertNewItems(items: List<Item>, feed: Feed) {
|
||||
items.sortedWith(Item::compareTo) // TODO Check if ordering is useful in this situation
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package com.readrops.app.compose.util.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import com.readrops.app.compose.util.theme.VeryShortSpacer
|
||||
|
||||
@Composable
|
||||
|
@ -12,6 +15,24 @@ fun RefreshScreen(
|
|||
feedMax: Int
|
||||
) {
|
||||
CenteredColumn {
|
||||
RefreshIndicator(
|
||||
currentFeed = currentFeed,
|
||||
feedCount = feedCount,
|
||||
feedMax = feedMax
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RefreshIndicator(
|
||||
currentFeed: String,
|
||||
feedCount: Int,
|
||||
feedMax: Int
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
progress = { feedCount.toFloat() / feedMax.toFloat() }
|
||||
)
|
||||
|
@ -19,7 +40,8 @@ fun RefreshScreen(
|
|||
VeryShortSpacer()
|
||||
|
||||
Text(
|
||||
text = "$currentFeed ($feedCount/$feedMax)"
|
||||
text = "$currentFeed ($feedCount/$feedMax)",
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
|
||||
</vector>
|
|
@ -93,7 +93,7 @@
|
|||
<string name="external_navigator">Navigateur externe</string>
|
||||
<string name="actualize">Actualiser</string>
|
||||
<string name="share_url">Partager le lien</string>
|
||||
<string name="opml_import_export">Importation/Exportation OPML</string>
|
||||
<string name="opml_import_export">Import/Export OPML</string>
|
||||
<string name="opml_processing">Traitement du fichier OPML</string>
|
||||
<string name="operation_takes_time">Cette opération peut prendre un certain temps car il faut interroger chaque flux.</string>
|
||||
<string name="processing_file_failed">Une erreur s\'est produite lors du traitement du fichier</string>
|
||||
|
|
|
@ -18,4 +18,7 @@ interface NewFolderDao : NewBaseDao<Folder> {
|
|||
|
||||
@Query("Select * From Folder Where account_id = :accountId")
|
||||
fun selectFolders(accountId: Int): Flow<List<Folder>>
|
||||
|
||||
@Query("Select * From Folder Where name = :name And account_id = :accountId")
|
||||
suspend fun selectFolderByName(name: String, accountId: Int): Folder?
|
||||
}
|
Loading…
Reference in New Issue