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..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 @@ -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,27 @@ 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/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/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/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..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 @@ -1,12 +1,24 @@ 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 43fa0fba3..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 @@ -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,26 @@ 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.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 +import java.util.Date @OptIn(ExperimentalAnimationApi::class) @Composable @@ -54,6 +80,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 +91,7 @@ fun AccountDetailsPage( } val launcher = rememberLauncherForActivityResult( - ActivityResultContracts.CreateDocument() + ActivityResultContracts.CreateDocument("*/*") ) { result -> viewModel.exportAsOPML(selectedAccount!!.id!!) { string -> result?.let { uri -> @@ -184,7 +211,7 @@ fun AccountDetailsPage( SettingItem( title = stringResource(R.string.export_as_opml), onClick = { - launcher.launch("ReadYou.opml") + exportOPMLModeDialogVisible = true }, ) {} SettingItem( @@ -374,4 +401,38 @@ 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.include_additional_info), + selected = uiState.exportOPMLMode == ExportOPMLMode.ATTACH_INFO, + ) { + viewModel.changeExportOPMLMode(ExportOPMLMode.ATTACH_INFO) + launcherOPMLFile(context, launcher) + }, + RadioDialogOption( + text = stringResource(R.string.exclude), + 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().toString(DateFormat.YYYY_MM_DD_DASH_HH_MM_SS)}.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..a99ae18e2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -261,4 +261,7 @@ 强制使用默认浏览器 “打开链接”设置被忽略,因为出现了错误。 未选择 - \ No newline at end of file + 包含附加信息 + 不包含 + 附加信息中包含了每个订阅源的配置选项,例如是否允许通知、是否全文解析等。当您期望将导出的 OPML 文件用于其他阅读器时,请选择“不包含”。 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83e0177be..231f2baf3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,4 +403,7 @@ Browser \"Open Link\" setting ignored because something went wrong. Open with… - \ No newline at end of file + 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\". +