diff --git a/api/build.gradle b/api/build.gradle index a50c52c4..f30f3e18 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -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) } diff --git a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt index d7d3fe0e..3717c145 100644 --- a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt +++ b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt @@ -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>> { - return Single.create { emitter -> - try { - val adapter = OPMLAdapter() - val opml = adapter.fromXml(stream.konsumeXml()) + suspend fun read(stream: InputStream): Map> { + 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>, outputStream: OutputStream): Completable { - return Completable.create { emitter -> - val opml = xml("opml") { - attribute("version", "2.0") + suspend fun write(foldersAndFeeds: Map>, 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() } } \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/utils/ApiUtils.kt b/api/src/main/java/com/readrops/api/utils/ApiUtils.kt index 0238be21..e8656b88 100644 --- a/api/src/main/java/com/readrops/api/utils/ApiUtils.kt +++ b/api/src/main/java/com/readrops/api/utils/ApiUtils.kt @@ -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 = diff --git a/api/src/test/java/com/readrops/api/opml/OPMLParserTest.kt b/api/src/test/java/com/readrops/api/opml/OPMLParserTest.kt index 32d3fd56..e0fca210 100644 --- a/api/src/test/java/com/readrops/api/opml/OPMLParserTest.kt +++ b/api/src/test/java/com/readrops/api/opml/OPMLParserTest.kt @@ -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>? = 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>? = 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> = HashMap>().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>? = 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() } diff --git a/app/src/main/java/com/readrops/app/account/AccountViewModel.java b/app/src/main/java/com/readrops/app/account/AccountViewModel.java index 57e2175c..99a37586 100644 --- a/app/src/main/java/com/readrops/app/account/AccountViewModel.java +++ b/app/src/main/java/com/readrops/app/account/AccountViewModel.java @@ -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(); } } diff --git a/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java index 60ba3bb7..d604eb3d 100644 --- a/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java +++ b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java @@ -257,8 +257,8 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { .blockingGet(); - OPMLParser.write(folderListMap, outputStream) - .blockingAwait(); + /*OPMLParser.write(folderListMap, outputStream) + .blockingAwait();*/ return Unit.INSTANCE; }); diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreenModel.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreenModel.kt index 44b68429..f941c7a0 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreenModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreenModel.kt @@ -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 } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index fea6e23c..6d781e9d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -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() 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), diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/OPMLImportProgressDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/account/OPMLImportProgressDialog.kt new file mode 100644 index 00000000..57e11443 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/account/OPMLImportProgressDialog.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreenModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreenModel.kt index 28cc76ac..d7287421 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreenModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreenModel.kt @@ -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) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt index e9d2dcfa..c3909fb4 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt @@ -37,7 +37,7 @@ abstract class ARepository( */ abstract suspend fun synchronize(): SyncResult - abstract suspend fun insertNewFeeds(urls: List) + abstract suspend fun insertNewFeeds(newFeeds: List, 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>, + 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 + ) + } + } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt index b2d320d0..0bd18690 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt @@ -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) = 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, 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, feed: Feed) { items.sortedWith(Item::compareTo) // TODO Check if ordering is useful in this situation diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/RefreshScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/RefreshScreen.kt index ad9b0a56..6e31fd0a 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/util/components/RefreshScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/RefreshScreen.kt @@ -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 ) } } \ No newline at end of file diff --git a/appcompose/src/main/res/drawable/ic_import_export.xml b/appcompose/src/main/res/drawable/ic_import_export.xml new file mode 100644 index 00000000..99f71b7f --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_import_export.xml @@ -0,0 +1,5 @@ + + + diff --git a/appcompose/src/main/res/values-fr/strings.xml b/appcompose/src/main/res/values-fr/strings.xml index 0ec147b2..ff91d6c5 100644 --- a/appcompose/src/main/res/values-fr/strings.xml +++ b/appcompose/src/main/res/values-fr/strings.xml @@ -93,7 +93,7 @@ Navigateur externe Actualiser Partager le lien - Importation/Exportation OPML + Import/Export OPML Traitement du fichier OPML Cette opération peut prendre un certain temps car il faut interroger chaque flux. Une erreur s\'est produite lors du traitement du fichier diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt index 175a8dcc..ac7ec4ce 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt @@ -18,4 +18,7 @@ interface NewFolderDao : NewBaseDao { @Query("Select * From Folder Where account_id = :accountId") fun selectFolders(accountId: Int): Flow> + + @Query("Select * From Folder Where name = :name And account_id = :accountId") + suspend fun selectFolderByName(name: String, accountId: Int): Folder? } \ No newline at end of file