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'
|
debugApi 'com.chimerapps.niddler:niddler:1.5.5'
|
||||||
releaseApi 'com.chimerapps.niddler:niddler-noop: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
|
package com.readrops.api.opml
|
||||||
|
|
||||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||||
|
import com.readrops.api.utils.exceptions.ParseException
|
||||||
import com.readrops.db.entities.Feed
|
import com.readrops.db.entities.Feed
|
||||||
import com.readrops.db.entities.Folder
|
import com.readrops.db.entities.Folder
|
||||||
import io.reactivex.Completable
|
|
||||||
import io.reactivex.Single
|
|
||||||
import org.redundent.kotlin.xml.xml
|
import org.redundent.kotlin.xml.xml
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
@ -12,22 +11,20 @@ import java.io.OutputStream
|
||||||
object OPMLParser {
|
object OPMLParser {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun read(stream: InputStream): Single<Map<Folder?, List<Feed>>> {
|
suspend fun read(stream: InputStream): Map<Folder?, List<Feed>> {
|
||||||
return Single.create { emitter ->
|
|
||||||
try {
|
try {
|
||||||
val adapter = OPMLAdapter()
|
val adapter = OPMLAdapter()
|
||||||
val opml = adapter.fromXml(stream.konsumeXml())
|
val opml = adapter.fromXml(stream.konsumeXml())
|
||||||
|
|
||||||
emitter.onSuccess(opml)
|
stream.close()
|
||||||
|
return opml
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emitter.onError(e)
|
throw ParseException(e.message)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun write(foldersAndFeeds: Map<Folder?, List<Feed>>, outputStream: OutputStream): Completable {
|
suspend fun write(foldersAndFeeds: Map<Folder?, List<Feed>>, outputStream: OutputStream) {
|
||||||
return Completable.create { emitter ->
|
|
||||||
val opml = xml("opml") {
|
val opml = xml("opml") {
|
||||||
attribute("version", "2.0")
|
attribute("version", "2.0")
|
||||||
|
|
||||||
|
@ -67,8 +64,5 @@ object OPMLParser {
|
||||||
|
|
||||||
outputStream.write(opml.toString().toByteArray())
|
outputStream.write(opml.toString().toByteArray())
|
||||||
outputStream.flush()
|
outputStream.flush()
|
||||||
|
|
||||||
emitter.onComplete()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,6 +16,8 @@ object ApiUtils {
|
||||||
const val HTTP_NOT_FOUND = 404
|
const val HTTP_NOT_FOUND = 404
|
||||||
const val HTTP_CONFLICT = 409
|
const val HTTP_CONFLICT = 409
|
||||||
|
|
||||||
|
val OPML_MIMETYPES = listOf("application/xml", "text/xml", "text/x-opml")
|
||||||
|
|
||||||
private const val RSS_CONTENT_TYPE_REGEX = "([^;]+)"
|
private const val RSS_CONTENT_TYPE_REGEX = "([^;]+)"
|
||||||
|
|
||||||
fun isMimeImage(type: String): Boolean =
|
fun isMimeImage(type: String): Boolean =
|
||||||
|
|
|
@ -4,8 +4,8 @@ import com.readrops.api.TestUtils
|
||||||
import com.readrops.api.utils.exceptions.ParseException
|
import com.readrops.api.utils.exceptions.ParseException
|
||||||
import com.readrops.db.entities.Feed
|
import com.readrops.db.entities.Feed
|
||||||
import com.readrops.db.entities.Folder
|
import com.readrops.db.entities.Folder
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
@ -13,83 +13,85 @@ import java.io.FileOutputStream
|
||||||
class OPMLParserTest {
|
class OPMLParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun readOpmlTest() {
|
fun readOpmlTest() = runTest {
|
||||||
val stream = TestUtils.loadResource("opml/subscriptions.opml")
|
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)
|
assertEquals(foldersAndFeeds[Folder(name = "Folder 1")]?.size, 2)
|
||||||
.observeOn(Schedulers.trampoline())
|
assertEquals(foldersAndFeeds[Folder(name = "Subfolder 1")]?.size, 4)
|
||||||
.subscribeOn(Schedulers.trampoline())
|
assertEquals(foldersAndFeeds[Folder(name = "Subfolder 2")]?.size, 1)
|
||||||
.subscribe { result -> foldersAndFeeds = result }
|
assertEquals(foldersAndFeeds[Folder(name = "Sub subfolder 1")]?.size, 2)
|
||||||
|
assertEquals(foldersAndFeeds[Folder(name = "Sub subfolder 2")]?.size, 0)
|
||||||
assertEquals(foldersAndFeeds?.size, 6)
|
assertEquals(foldersAndFeeds[null]?.size, 2)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
stream.close()
|
stream.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun readLiteSubscriptionsTest() {
|
fun readLiteSubscriptionsTest() = runTest {
|
||||||
val stream = TestUtils.loadResource("opml/lite_subscriptions.opml")
|
val stream = TestUtils.loadResource("opml/lite_subscriptions.opml")
|
||||||
|
|
||||||
var foldersAndFeeds: Map<Folder?, List<Feed>>? = null
|
val foldersAndFeeds = OPMLParser.read(stream)
|
||||||
|
|
||||||
OPMLParser.read(stream)
|
assertEquals(foldersAndFeeds.values.first().size, 2)
|
||||||
.subscribe { result -> foldersAndFeeds = result }
|
assertEquals(
|
||||||
|
foldersAndFeeds.values.first().first().url,
|
||||||
assertEquals(foldersAndFeeds?.values?.first()?.size, 2)
|
"http://www.theverge.com/rss/index.xml"
|
||||||
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()[1].url, "https://techcrunch.com/feed/")
|
||||||
|
|
||||||
stream.close()
|
stream.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test(expected = ParseException::class)
|
||||||
fun opmlVersionTest() {
|
fun opmlVersionTest() = runTest {
|
||||||
val stream = TestUtils.loadResource("opml/wrong_version.opml")
|
val stream = TestUtils.loadResource("opml/wrong_version.opml")
|
||||||
|
|
||||||
OPMLParser.read(stream)
|
OPMLParser.read(stream)
|
||||||
.test()
|
|
||||||
.assertError(ParseException::class.java)
|
|
||||||
|
|
||||||
stream.close()
|
stream.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun writeOpmlTest() {
|
fun writeOpmlTest() = runTest {
|
||||||
val file = File("subscriptions.opml")
|
val file = File("subscriptions.opml")
|
||||||
val outputStream = FileOutputStream(file)
|
val outputStream = FileOutputStream(file)
|
||||||
|
|
||||||
val foldersAndFeeds: Map<Folder?, List<Feed>> = HashMap<Folder?, List<Feed>>().apply {
|
val foldersAndFeeds: Map<Folder?, List<Feed>> = HashMap<Folder?, List<Feed>>().apply {
|
||||||
put(null, listOf(Feed(name = "Feed1", url = "https://feed1.com"),
|
put(
|
||||||
Feed(name = "Feed2", url = "https://feed2.com")))
|
null, listOf(
|
||||||
|
Feed(name = "Feed1", url = "https://feed1.com"),
|
||||||
|
Feed(name = "Feed2", url = "https://feed2.com")
|
||||||
|
)
|
||||||
|
)
|
||||||
put(Folder(name = "Folder1"), listOf())
|
put(Folder(name = "Folder1"), listOf())
|
||||||
put(Folder(name = "Folder2"), listOf(Feed(name = "Feed3", url = "https://feed3.com"),
|
put(
|
||||||
Feed(name = "Feed4", url ="https://feed4.com")))
|
Folder(name = "Folder2"), listOf(
|
||||||
|
Feed(name = "Feed3", url = "https://feed3.com"),
|
||||||
|
Feed(name = "Feed4", url = "https://feed4.com")
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OPMLParser.write(foldersAndFeeds, outputStream)
|
OPMLParser.write(foldersAndFeeds, outputStream)
|
||||||
.subscribeOn(Schedulers.trampoline())
|
|
||||||
.subscribe()
|
|
||||||
|
|
||||||
outputStream.flush()
|
outputStream.flush()
|
||||||
outputStream.close()
|
outputStream.close()
|
||||||
|
|
||||||
val inputStream = file.inputStream()
|
val inputStream = file.inputStream()
|
||||||
var foldersAndFeeds2: Map<Folder?, List<Feed>>? = null
|
val foldersAndFeeds2 = OPMLParser.read(inputStream)
|
||||||
OPMLParser.read(inputStream).subscribe { result -> foldersAndFeeds2 = result }
|
|
||||||
|
|
||||||
assertEquals(foldersAndFeeds.size, foldersAndFeeds2?.size)
|
assertEquals(foldersAndFeeds.size, foldersAndFeeds2.size)
|
||||||
assertEquals(foldersAndFeeds[Folder(name = "Folder1")]?.size, foldersAndFeeds2?.get(Folder(name = "Folder1"))?.size)
|
assertEquals(
|
||||||
assertEquals(foldersAndFeeds[Folder(name = "Folder2")]?.size, foldersAndFeeds2?.get(Folder(name = "Folder2"))?.size)
|
foldersAndFeeds[Folder(name = "Folder1")]?.size,
|
||||||
assertEquals(foldersAndFeeds[null]?.size, foldersAndFeeds2?.get(null)?.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()
|
inputStream.close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,8 @@ public class AccountViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Completable parseOPMLFile(Uri uri, Context context) throws FileNotFoundException {
|
public Completable parseOPMLFile(Uri uri, Context context) throws FileNotFoundException {
|
||||||
return OPMLParser.read(context.getContentResolver().openInputStream(uri))
|
/*return OPMLParser.read(context.getContentResolver().openInputStream(uri))
|
||||||
.flatMapCompletable(foldersAndFeeds -> repository.insertOPMLFoldersAndFeeds(foldersAndFeeds));
|
.flatMapCompletable(foldersAndFeeds -> repository.insertOPMLFoldersAndFeeds(foldersAndFeeds));*/
|
||||||
|
return Completable.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -257,8 +257,8 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
||||||
.blockingGet();
|
.blockingGet();
|
||||||
|
|
||||||
|
|
||||||
OPMLParser.write(folderListMap, outputStream)
|
/*OPMLParser.write(folderListMap, outputStream)
|
||||||
.blockingAwait();
|
.blockingAwait();*/
|
||||||
|
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.readrops.app.compose.account
|
package com.readrops.app.compose.account
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import com.readrops.api.opml.OPMLParser
|
||||||
import com.readrops.app.compose.base.TabScreenModel
|
import com.readrops.app.compose.base.TabScreenModel
|
||||||
import com.readrops.db.Database
|
import com.readrops.db.Database
|
||||||
import com.readrops.db.entities.account.Account
|
import com.readrops.db.entities.account.Account
|
||||||
|
@ -45,6 +48,39 @@ class AccountScreenModel(
|
||||||
_closeHome.update { true }
|
_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(
|
data class AccountState(
|
||||||
|
@ -55,4 +91,6 @@ data class AccountState(
|
||||||
sealed interface DialogState {
|
sealed interface DialogState {
|
||||||
object DeleteAccount : DialogState
|
object DeleteAccount : DialogState
|
||||||
object NewAccount : 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
|
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.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -24,6 +26,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
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.currentOrThrow
|
||||||
import cafe.adriel.voyager.navigator.tab.Tab
|
import cafe.adriel.voyager.navigator.tab.Tab
|
||||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import com.readrops.api.utils.ApiUtils
|
||||||
import com.readrops.app.compose.R
|
import com.readrops.app.compose.R
|
||||||
import com.readrops.app.compose.account.credentials.AccountCredentialsScreen
|
import com.readrops.app.compose.account.credentials.AccountCredentialsScreen
|
||||||
import com.readrops.app.compose.account.selection.AccountSelectionDialog
|
import com.readrops.app.compose.account.selection.AccountSelectionDialog
|
||||||
|
@ -56,6 +60,7 @@ object AccountTab : Tab {
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val context = LocalContext.current
|
||||||
val viewModel = getScreenModel<AccountScreenModel>()
|
val viewModel = getScreenModel<AccountScreenModel>()
|
||||||
|
|
||||||
val closeHome by viewModel.closeHome.collectAsStateWithLifecycle()
|
val closeHome by viewModel.closeHome.collectAsStateWithLifecycle()
|
||||||
|
@ -65,8 +70,13 @@ object AccountTab : Tab {
|
||||||
navigator.replaceAll(AccountSelectionScreen())
|
navigator.replaceAll(AccountSelectionScreen())
|
||||||
}
|
}
|
||||||
|
|
||||||
when (state.dialog) {
|
val launcher =
|
||||||
DialogState.DeleteAccount -> {
|
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||||
|
uri?.let { viewModel.parseOPMLFile(uri, context) }
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val dialog = state.dialog) {
|
||||||
|
is DialogState.DeleteAccount -> {
|
||||||
TwoChoicesDialog(
|
TwoChoicesDialog(
|
||||||
title = stringResource(R.string.delete_account),
|
title = stringResource(R.string.delete_account),
|
||||||
text = stringResource(R.string.delete_account_question),
|
text = stringResource(R.string.delete_account_question),
|
||||||
|
@ -81,7 +91,7 @@ object AccountTab : Tab {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DialogState.NewAccount -> {
|
is DialogState.NewAccount -> {
|
||||||
AccountSelectionDialog(
|
AccountSelectionDialog(
|
||||||
onDismiss = { viewModel.closeDialog() },
|
onDismiss = { viewModel.closeDialog() },
|
||||||
onValidate = { accountType ->
|
onValidate = { accountType ->
|
||||||
|
@ -91,6 +101,14 @@ object AccountTab : Tab {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is DialogState.OPMLImport -> {
|
||||||
|
OPMLImportProgressDialog(
|
||||||
|
currentFeed = dialog.currentFeed,
|
||||||
|
feedCount = dialog.feedCount,
|
||||||
|
feedMax = dialog.feedMax
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +182,15 @@ object AccountTab : Tab {
|
||||||
onClick = { }
|
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(
|
SelectableIconText(
|
||||||
icon = rememberVectorPainter(image = Icons.Default.AccountCircle),
|
icon = rememberVectorPainter(image = Icons.Default.AccountCircle),
|
||||||
text = stringResource(R.string.delete_account),
|
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 {
|
try {
|
||||||
if (localRSSDataSource.isUrlRSSResource(url)) {
|
if (localRSSDataSource.isUrlRSSResource(url)) {
|
||||||
// TODO add support for all account types
|
// TODO add support for all account types
|
||||||
repository?.insertNewFeeds(listOf(url))
|
repository?.insertNewFeeds(
|
||||||
|
newFeeds = listOf(Feed(url = url))
|
||||||
|
) {}
|
||||||
|
|
||||||
closeDialog(DialogState.AddFeed)
|
closeDialog(DialogState.AddFeed)
|
||||||
} else {
|
} else {
|
||||||
|
@ -200,7 +202,9 @@ class FeedScreenModel(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO add support for all account types
|
// 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)
|
closeDialog(DialogState.AddFeed)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ abstract class ARepository(
|
||||||
*/
|
*/
|
||||||
abstract suspend fun synchronize(): SyncResult
|
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(
|
abstract class BaseRepository(
|
||||||
|
@ -45,7 +45,8 @@ abstract class BaseRepository(
|
||||||
account: Account,
|
account: Account,
|
||||||
) : ARepository(database, 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)
|
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) {
|
open suspend fun setAllItemsReadByFolder(folderId: Int, accountId: Int) {
|
||||||
database.newItemDao().setAllItemsReadByFolder(folderId, accountId)
|
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
|
package com.readrops.app.compose.repositories
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.readrops.api.localfeed.LocalRSSDataSource
|
import com.readrops.api.localfeed.LocalRSSDataSource
|
||||||
import com.readrops.api.services.SyncResult
|
import com.readrops.api.services.SyncResult
|
||||||
import com.readrops.api.utils.ApiUtils
|
import com.readrops.api.utils.ApiUtils
|
||||||
|
@ -68,13 +69,16 @@ class LocalRSSRepository(
|
||||||
throw NotImplementedError("This method can't be called here")
|
throw NotImplementedError("This method can't be called here")
|
||||||
|
|
||||||
|
|
||||||
override suspend fun insertNewFeeds(urls: List<String>) = withContext(Dispatchers.IO) {
|
override suspend fun insertNewFeeds(newFeeds: List<Feed>, onUpdate: (Feed) -> Unit) = withContext(Dispatchers.IO) {
|
||||||
for (url in urls) {
|
for (newFeed in newFeeds) {
|
||||||
|
onUpdate(newFeed)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val result = dataSource.queryRSSResource(url, null)!!
|
val result = dataSource.queryRSSResource(newFeed.url!!, null)!!
|
||||||
insertFeed(result.first)
|
insertFeed(result.first.also { it.folderId = newFeed.folderId })
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw e
|
Log.d("LocalRSSRepository", e.message.orEmpty())
|
||||||
|
//throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package com.readrops.app.compose.util.components
|
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.LinearProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import com.readrops.app.compose.util.theme.VeryShortSpacer
|
import com.readrops.app.compose.util.theme.VeryShortSpacer
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -12,6 +15,24 @@ fun RefreshScreen(
|
||||||
feedMax: Int
|
feedMax: Int
|
||||||
) {
|
) {
|
||||||
CenteredColumn {
|
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(
|
LinearProgressIndicator(
|
||||||
progress = { feedCount.toFloat() / feedMax.toFloat() }
|
progress = { feedCount.toFloat() / feedMax.toFloat() }
|
||||||
)
|
)
|
||||||
|
@ -19,7 +40,8 @@ fun RefreshScreen(
|
||||||
VeryShortSpacer()
|
VeryShortSpacer()
|
||||||
|
|
||||||
Text(
|
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="external_navigator">Navigateur externe</string>
|
||||||
<string name="actualize">Actualiser</string>
|
<string name="actualize">Actualiser</string>
|
||||||
<string name="share_url">Partager le lien</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="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="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>
|
<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")
|
@Query("Select * From Folder Where account_id = :accountId")
|
||||||
fun selectFolders(accountId: Int): Flow<List<Folder>>
|
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