From 69d225cb0622ae6f44956837df064bcb37d855af Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 26 Jan 2024 12:20:59 +0800 Subject: [PATCH 1/3] feat(export): add attach info option when export as opml file --- .../ash/reader/domain/service/OpmlService.kt | 23 ++++--- .../ui/component/base/RYSelectionChip.kt | 13 +++- .../settings/accounts/AccountDetailsPage.kt | 62 +++++++++++++++++-- .../settings/accounts/AccountViewModel.kt | 26 +++++++- app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values/strings.xml | 4 +- 6 files changed, 114 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/me/ash/reader/domain/service/OpmlService.kt b/app/src/main/java/me/ash/reader/domain/service/OpmlService.kt index 9e08d7c45..6043b6022 100644 --- a/app/src/main/java/me/ash/reader/domain/service/OpmlService.kt +++ b/app/src/main/java/me/ash/reader/domain/service/OpmlService.kt @@ -60,7 +60,7 @@ class OpmlService @Inject constructor( * Exports OPML file. */ @Throws(Exception::class) - suspend fun saveToString(accountId: Int): String { + suspend fun saveToString(accountId: Int, attachInfo: Boolean): String { val defaultGroup = groupDao.queryById(getDefaultGroupId(accountId))!! return OpmlWriter().write( Opml( @@ -73,21 +73,28 @@ class OpmlService @Inject constructor( ), Body(groupDao.queryAllGroupWithFeed(accountId).map { Outline( - mapOf( + mutableMapOf( "text" to it.group.name, "title" to it.group.name, "isDefault" to (it.group.id == defaultGroup.id).toString() - ), + ).apply { + if (attachInfo) { + put("isDefault", (it.group.id == defaultGroup.id).toString()) + } + }, it.feeds.map { feed -> Outline( - mapOf( + mutableMapOf( "text" to feed.name, "title" to feed.name, "xmlUrl" to feed.url, - "htmlUrl" to feed.url, - "isNotification" to feed.isNotification.toString(), - "isFullContent" to feed.isFullContent.toString(), - ), + "htmlUrl" to feed.url + ).apply { + if (attachInfo) { + put("isNotification", feed.isNotification.toString()) + put("isFullContent", feed.isFullContent.toString()) + } + }, listOf() ) } diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RYSelectionChip.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYSelectionChip.kt index 68b14a934..b3e9cfe68 100644 --- a/app/src/main/java/me/ash/reader/ui/component/base/RYSelectionChip.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYSelectionChip.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import me.ash.reader.R import me.ash.reader.ui.theme.palette.alwaysLight @@ -79,4 +80,14 @@ fun RYSelectionChip( ) }, ) -} \ No newline at end of file +} + +@Preview +@Composable +private fun RYSelectionChipPreview() { + RYSelectionChip( + content = "Test", + selected = true, + onClick = {}, + ) +} diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt index 43fa0fba3..49cd3cc00 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt @@ -1,9 +1,17 @@ package me.ash.reader.ui.page.settings.accounts +import android.content.Context +import android.net.Uri +import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DeleteSweep @@ -13,7 +21,12 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -26,13 +39,24 @@ import me.ash.reader.infrastructure.preference.KeepArchivedPreference import me.ash.reader.infrastructure.preference.SyncBlockListPreference import me.ash.reader.infrastructure.preference.SyncIntervalPreference import me.ash.reader.infrastructure.preference.not -import me.ash.reader.ui.component.base.* +import me.ash.reader.ui.component.base.DisplayText +import me.ash.reader.ui.component.base.FeedbackIconButton +import me.ash.reader.ui.component.base.RYDialog +import me.ash.reader.ui.component.base.RYScaffold +import me.ash.reader.ui.component.base.RYSwitch +import me.ash.reader.ui.component.base.RadioDialog +import me.ash.reader.ui.component.base.RadioDialogOption +import me.ash.reader.ui.component.base.Subtitle +import me.ash.reader.ui.component.base.TextFieldDialog +import me.ash.reader.ui.component.base.Tips import me.ash.reader.ui.ext.collectAsStateValue +import me.ash.reader.ui.ext.getCurrentVersion import me.ash.reader.ui.ext.showToast import me.ash.reader.ui.ext.showToastLong import me.ash.reader.ui.page.settings.SettingItem import me.ash.reader.ui.page.settings.accounts.connection.AccountConnection import me.ash.reader.ui.theme.palette.onLight +import java.util.Date @OptIn(ExperimentalAnimationApi::class) @Composable @@ -54,6 +78,7 @@ fun AccountDetailsPage( var blockListDialogVisible by remember { mutableStateOf(false) } var syncIntervalDialogVisible by remember { mutableStateOf(false) } var keepArchivedDialogVisible by remember { mutableStateOf(false) } + var exportOPMLModeDialogVisible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { navController.currentBackStackEntryFlow.collect { @@ -64,7 +89,7 @@ fun AccountDetailsPage( } val launcher = rememberLauncherForActivityResult( - ActivityResultContracts.CreateDocument() + ActivityResultContracts.CreateDocument("*/*") ) { result -> viewModel.exportAsOPML(selectedAccount!!.id!!) { string -> result?.let { uri -> @@ -184,7 +209,7 @@ fun AccountDetailsPage( SettingItem( title = stringResource(R.string.export_as_opml), onClick = { - launcher.launch("ReadYou.opml") + exportOPMLModeDialogVisible = true }, ) {} SettingItem( @@ -374,4 +399,31 @@ fun AccountDetailsPage( } }, ) + + RadioDialog( + visible = exportOPMLModeDialogVisible, + title = stringResource(R.string.export_as_opml), + options = listOf( + RadioDialogOption( + text = stringResource(R.string.attach_info), + selected = uiState.exportOPMLMode == ExportOPMLMode.ATTACH_INFO, + ) { + viewModel.changeExportOPMLMode(ExportOPMLMode.ATTACH_INFO) + launcherOPMLFile(context, launcher) + }, + RadioDialogOption( + text = stringResource(R.string.no_attach), + selected = uiState.exportOPMLMode == ExportOPMLMode.NO_ATTACH, + ) { + viewModel.changeExportOPMLMode(ExportOPMLMode.NO_ATTACH) + launcherOPMLFile(context, launcher) + } + ) + ) { + exportOPMLModeDialogVisible = false + } +} + +private fun launcherOPMLFile(context: Context, launcher: ManagedActivityResultLauncher) { + launcher.launch("${context.getString(R.string.read_you)}-${context.getCurrentVersion()}-export-${Date()}.opml") } diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt index 4b3c2a82f..7838ef29d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt @@ -5,7 +5,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.ash.reader.domain.model.account.Account @@ -50,7 +55,8 @@ class AccountViewModel @Inject constructor( fun exportAsOPML(accountId: Int, callback: (String) -> Unit = {}) { viewModelScope.launch(defaultDispatcher) { try { - callback(opmlService.saveToString(accountId)) + callback(opmlService.saveToString(accountId, + _accountUiState.value.exportOPMLMode == ExportOPMLMode.ATTACH_INFO)) } catch (e: Exception) { Log.e("FeedsViewModel", "exportAsOpml: ", e) } @@ -119,10 +125,26 @@ class AccountViewModel @Inject constructor( } } } + + fun changeExportOPMLMode(mode: ExportOPMLMode) { + viewModelScope.launch { + _accountUiState.update { + it.copy( + exportOPMLMode = mode + ) + } + } + } } data class AccountUiState( val selectedAccount: Flow = emptyFlow(), val deleteDialogVisible: Boolean = false, val clearDialogVisible: Boolean = false, + val exportOPMLMode: ExportOPMLMode = ExportOPMLMode.ATTACH_INFO, ) + +sealed class ExportOPMLMode { + object ATTACH_INFO : ExportOPMLMode() + object NO_ATTACH : ExportOPMLMode() +} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 48e3d1982..852d1bc15 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -261,4 +261,6 @@ 强制使用默认浏览器 “打开链接”设置被忽略,因为出现了错误。 未选择 - \ No newline at end of file + 附加信息 + 无附加 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83e0177be..8ed8bccfe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,4 +403,6 @@ Browser \"Open Link\" setting ignored because something went wrong. Open with… - \ No newline at end of file + Attach info + No attach + From 51838837b1f62677c2ee1be54265aff21db9e4b9 Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 26 Jan 2024 14:30:04 +0800 Subject: [PATCH 2/3] feat(export): add attach info option when export as opml file --- .../me/ash/reader/domain/service/OpmlService.kt | 1 - .../infrastructure/android/CrashHandler.kt | 3 +-- .../ash/reader/ui/component/base/RadioDialog.kt | 11 +++++++++++ .../main/java/me/ash/reader/ui/ext/DateExt.kt | 15 +++++++++++++-- .../settings/accounts/AccountDetailsPage.kt | 17 +++++++++++++---- app/src/main/res/values-zh-rCN/strings.xml | 5 +++-- app/src/main/res/values/strings.xml | 5 +++-- 7 files changed, 44 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/me/ash/reader/domain/service/OpmlService.kt b/app/src/main/java/me/ash/reader/domain/service/OpmlService.kt index 6043b6022..f5196e728 100644 --- a/app/src/main/java/me/ash/reader/domain/service/OpmlService.kt +++ b/app/src/main/java/me/ash/reader/domain/service/OpmlService.kt @@ -76,7 +76,6 @@ class OpmlService @Inject constructor( mutableMapOf( "text" to it.group.name, "title" to it.group.name, - "isDefault" to (it.group.id == defaultGroup.id).toString() ).apply { if (attachInfo) { put("isDefault", (it.group.id == defaultGroup.id).toString()) diff --git a/app/src/main/java/me/ash/reader/infrastructure/android/CrashHandler.kt b/app/src/main/java/me/ash/reader/infrastructure/android/CrashHandler.kt index b8dd6fdec..cef2682bc 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/android/CrashHandler.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/android/CrashHandler.kt @@ -20,11 +20,10 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler { */ override fun uncaughtException(p0: Thread, p1: Throwable) { val causeMessage = getCauseMessage(p1) - Log.e("RLog", "uncaughtException: $causeMessage") + Log.e("RLog", "uncaughtException: $causeMessage", p1) Looper.myLooper() ?: Looper.prepare() context.showToastLong(causeMessage) Looper.loop() - p1.printStackTrace() // android.os.Process.killProcess(android.os.Process.myPid()); // exitProcess(1) } diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RadioDialog.kt b/app/src/main/java/me/ash/reader/ui/component/base/RadioDialog.kt index 535d7e071..02702c808 100644 --- a/app/src/main/java/me/ash/reader/ui/component/base/RadioDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RadioDialog.kt @@ -2,7 +2,9 @@ package me.ash.reader.ui.component.base import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -27,6 +29,7 @@ fun RadioDialog( modifier: Modifier = Modifier, visible: Boolean = false, title: String = "", + description: String? = null, options: List = emptyList(), onDismissRequest: () -> Unit = {}, ) { @@ -44,6 +47,14 @@ fun RadioDialog( }, text = { LazyColumn { + if (description != null) { + item { + Text(text = description) + if (options.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + } + } + } items(options) { option -> Row( modifier = Modifier diff --git a/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt index 3eaa00d49..b9f571a2f 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt @@ -1,12 +1,23 @@ package me.ash.reader.ui.ext +import android.annotation.SuppressLint import android.content.Context import androidx.core.os.ConfigurationCompat import me.ash.reader.R -import java.text.DateFormat import java.text.ParsePosition import java.text.SimpleDateFormat -import java.util.* +import java.util.Calendar +import java.util.Date + +@SuppressLint("SimpleDateFormat") +object DateFormat { + val YYYY_MM_DD_HH_MM_SS = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + val YYYY_MM_DD_DASH_HH_MM_SS = SimpleDateFormat("yyyy-MM-dd-HH:mm:ss") +} + +fun Date.toString(format: SimpleDateFormat): String { + return format.format(this) +} fun Date.formatAsString( context: Context, diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt index 49cd3cc00..9ce999488 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt @@ -49,10 +49,12 @@ import me.ash.reader.ui.component.base.RadioDialogOption import me.ash.reader.ui.component.base.Subtitle import me.ash.reader.ui.component.base.TextFieldDialog import me.ash.reader.ui.component.base.Tips +import me.ash.reader.ui.ext.DateFormat import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.getCurrentVersion import me.ash.reader.ui.ext.showToast import me.ash.reader.ui.ext.showToastLong +import me.ash.reader.ui.ext.toString import me.ash.reader.ui.page.settings.SettingItem import me.ash.reader.ui.page.settings.accounts.connection.AccountConnection import me.ash.reader.ui.theme.palette.onLight @@ -403,16 +405,17 @@ fun AccountDetailsPage( RadioDialog( visible = exportOPMLModeDialogVisible, title = stringResource(R.string.export_as_opml), + description = stringResource(R.string.additional_info_desc), options = listOf( RadioDialogOption( - text = stringResource(R.string.attach_info), + text = stringResource(R.string.include_additional_info), selected = uiState.exportOPMLMode == ExportOPMLMode.ATTACH_INFO, ) { viewModel.changeExportOPMLMode(ExportOPMLMode.ATTACH_INFO) launcherOPMLFile(context, launcher) }, RadioDialogOption( - text = stringResource(R.string.no_attach), + text = stringResource(R.string.exclude), selected = uiState.exportOPMLMode == ExportOPMLMode.NO_ATTACH, ) { viewModel.changeExportOPMLMode(ExportOPMLMode.NO_ATTACH) @@ -424,6 +427,12 @@ fun AccountDetailsPage( } } -private fun launcherOPMLFile(context: Context, launcher: ManagedActivityResultLauncher) { - launcher.launch("${context.getString(R.string.read_you)}-${context.getCurrentVersion()}-export-${Date()}.opml") +private fun launcherOPMLFile( + context: Context, + launcher: ManagedActivityResultLauncher, +) { + launcher.launch("" + + "${context.getString(R.string.read_you)}-" + + "${context.getCurrentVersion()}-export-" + + "${Date().toString(DateFormat.YYYY_MM_DD_DASH_HH_MM_SS)}.opml") } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 852d1bc15..a99ae18e2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -261,6 +261,7 @@ 强制使用默认浏览器 “打开链接”设置被忽略,因为出现了错误。 未选择 - 附加信息 - 无附加 + 包含附加信息 + 不包含 + 附加信息中包含了每个订阅源的配置选项,例如是否允许通知、是否全文解析等。当您期望将导出的 OPML 文件用于其他阅读器时,请选择“不包含”。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ed8bccfe..231f2baf3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,6 +403,7 @@ Browser \"Open Link\" setting ignored because something went wrong. Open with… - Attach info - No attach + Include additional info + Exclude + Additional information includes configuration options for each feed, such as whether to allow notification, parse full content, etc. When you intend to use the exported OPML file with other readers, please select \"Exclude\". From c2fe8d45f54aa1d61f7bcab1cdd634b2edc6f0fe Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 26 Jan 2024 14:35:30 +0800 Subject: [PATCH 3/3] feat(export): add additional info option when export as opml file --- app/src/main/java/me/ash/reader/ui/ext/DateExt.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt index b9f571a2f..09a0911ee 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import androidx.core.os.ConfigurationCompat import me.ash.reader.R +import java.text.DateFormat import java.text.ParsePosition import java.text.SimpleDateFormat import java.util.Calendar