feat(settings): add troubleshooting page and app preferences import/export tool (#672)
This commit is contained in:
parent
d749107bea
commit
826819a10b
@ -94,12 +94,5 @@ class AccountService @Inject constructor(
|
|||||||
rssService.get().cancelSync()
|
rssService.get().cancelSync()
|
||||||
context.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!)
|
context.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!)
|
||||||
context.dataStore.put(DataStoreKeys.CurrentAccountType, account.type.id)
|
context.dataStore.put(DataStoreKeys.CurrentAccountType, account.type.id)
|
||||||
|
|
||||||
// Restart
|
|
||||||
// context.packageManager.getLaunchIntentForPackage(context.packageName)?.let {
|
|
||||||
// it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
||||||
// context.startActivity(it)
|
|
||||||
// android.os.Process.killProcess(android.os.Process.myPid())
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,7 @@ fun CrashReportPage(
|
|||||||
val startIndex = msg.indexOf(hyperLinkText)
|
val startIndex = msg.indexOf(hyperLinkText)
|
||||||
val endIndex = startIndex + hyperLinkText.length
|
val endIndex = startIndex + hyperLinkText.length
|
||||||
addUrlAnnotation(
|
addUrlAnnotation(
|
||||||
UrlAnnotation("https://github.com/Ashinch/ReadYou/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title="),
|
UrlAnnotation(stringResource(R.string.issue_tracer_url)),
|
||||||
start = startIndex,
|
start = startIndex,
|
||||||
end = endIndex
|
end = endIndex
|
||||||
)
|
)
|
||||||
|
@ -183,3 +183,9 @@ fun Context.getCustomTabsPackages(): List<String> {
|
|||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}.toList()
|
}.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.getPreferencesFile(): File =
|
||||||
|
File(filesDir.absolutePath + File.separator +
|
||||||
|
"datastore" + File.separator +
|
||||||
|
"settings.preferences_pb"
|
||||||
|
)
|
||||||
|
@ -27,9 +27,9 @@ val Context.skipVersionNumber: String
|
|||||||
val Context.isFirstLaunch: Boolean
|
val Context.isFirstLaunch: Boolean
|
||||||
get() = this.dataStore.get(DataStoreKeys.IsFirstLaunch) ?: true
|
get() = this.dataStore.get(DataStoreKeys.IsFirstLaunch) ?: true
|
||||||
val Context.currentAccountId: Int
|
val Context.currentAccountId: Int
|
||||||
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 1
|
||||||
val Context.currentAccountType: Int
|
val Context.currentAccountType: Int
|
||||||
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!!
|
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType) ?: 1
|
||||||
|
|
||||||
val Context.initialPage: Int
|
val Context.initialPage: Int
|
||||||
get() = this.dataStore.get(DataStoreKeys.InitialPage) ?: 0
|
get() = this.dataStore.get(DataStoreKeys.InitialPage) ?: 0
|
||||||
|
@ -36,3 +36,7 @@ fun File.mkDir() {
|
|||||||
if (!newF.exists()) newF.mkdir()
|
if (!newF.exists()) newF.mkdir()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ByteArray.isProbableProtobuf(): Boolean =
|
||||||
|
if (size < 2) false
|
||||||
|
else get(0) == 0x0a.toByte() && get(1) == 0x16.toByte()
|
||||||
|
@ -8,7 +8,7 @@ import java.security.MessageDigest
|
|||||||
object MimeType {
|
object MimeType {
|
||||||
|
|
||||||
const val ANY = "*/*"
|
const val ANY = "*/*"
|
||||||
const val FONT = "font/ttf" // Not supported yet
|
const val FONT = "font/ttf"
|
||||||
const val OPML = "text/x-opml" // Not supported yet
|
const val OPML = "text/x-opml" // Not supported yet
|
||||||
const val JSON = "application/json"
|
const val JSON = "application/json"
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ import me.ash.reader.ui.page.settings.interaction.InteractionPage
|
|||||||
import me.ash.reader.ui.page.settings.languages.LanguagesPage
|
import me.ash.reader.ui.page.settings.languages.LanguagesPage
|
||||||
import me.ash.reader.ui.page.settings.tips.LicenseListPage
|
import me.ash.reader.ui.page.settings.tips.LicenseListPage
|
||||||
import me.ash.reader.ui.page.settings.tips.TipsAndSupportPage
|
import me.ash.reader.ui.page.settings.tips.TipsAndSupportPage
|
||||||
|
import me.ash.reader.ui.page.settings.troubleshooting.TroubleshootingPage
|
||||||
import me.ash.reader.ui.page.startup.StartupPage
|
import me.ash.reader.ui.page.startup.StartupPage
|
||||||
import me.ash.reader.ui.theme.AppTheme
|
import me.ash.reader.ui.theme.AppTheme
|
||||||
|
|
||||||
@ -237,6 +238,11 @@ fun HomeEntry(
|
|||||||
LanguagesPage(navController = navController)
|
LanguagesPage(navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Troubleshooting
|
||||||
|
forwardAndBackwardComposable(route = RouteName.TROUBLESHOOTING) {
|
||||||
|
TroubleshootingPage(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
// Tips & Support
|
// Tips & Support
|
||||||
forwardAndBackwardComposable(route = RouteName.TIPS_AND_SUPPORT) {
|
forwardAndBackwardComposable(route = RouteName.TIPS_AND_SUPPORT) {
|
||||||
TipsAndSupportPage(navController)
|
TipsAndSupportPage(navController)
|
||||||
|
@ -36,6 +36,9 @@ object RouteName {
|
|||||||
// Languages
|
// Languages
|
||||||
const val LANGUAGES = "languages"
|
const val LANGUAGES = "languages"
|
||||||
|
|
||||||
|
// Troubleshooting
|
||||||
|
const val TROUBLESHOOTING = "troubleshooting"
|
||||||
|
|
||||||
// Tips & Support
|
// Tips & Support
|
||||||
const val TIPS_AND_SUPPORT = "tips_and_support"
|
const val TIPS_AND_SUPPORT = "tips_and_support"
|
||||||
const val LICENSE_LIST = "license_list"
|
const val LICENSE_LIST = "license_list"
|
||||||
|
@ -35,8 +35,10 @@ import me.ash.reader.ui.component.RenameDialog
|
|||||||
import me.ash.reader.ui.component.base.ClipboardTextField
|
import me.ash.reader.ui.component.base.ClipboardTextField
|
||||||
import me.ash.reader.ui.component.base.RYDialog
|
import me.ash.reader.ui.component.base.RYDialog
|
||||||
import me.ash.reader.ui.component.base.TextFieldDialog
|
import me.ash.reader.ui.component.base.TextFieldDialog
|
||||||
|
import me.ash.reader.ui.ext.MimeType
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
import me.ash.reader.ui.ext.roundClick
|
import me.ash.reader.ui.ext.roundClick
|
||||||
|
import me.ash.reader.ui.ext.showToast
|
||||||
import me.ash.reader.ui.page.home.feeds.FeedOptionView
|
import me.ash.reader.ui.page.home.feeds.FeedOptionView
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
@ -51,12 +53,12 @@ fun SubscribeDialog(
|
|||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val subscribeUiState = subscribeViewModel.subscribeUiState.collectAsStateValue()
|
val subscribeUiState = subscribeViewModel.subscribeUiState.collectAsStateValue()
|
||||||
val groupsState = subscribeUiState.groups.collectAsState(initial = emptyList())
|
val groupsState = subscribeUiState.groups.collectAsState(initial = emptyList())
|
||||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||||
it?.let { uri ->
|
it?.let { uri ->
|
||||||
context.contentResolver.openInputStream(uri)?.let { inputStream ->
|
context.contentResolver.openInputStream(uri)?.let { inputStream ->
|
||||||
subscribeViewModel.importFromInputStream(inputStream)
|
subscribeViewModel.importFromInputStream(inputStream)
|
||||||
}
|
} ?: context.showToast("Cannot open Input Stream with content resolver")
|
||||||
}
|
} ?: context.showToast("Cannot get activity result with launcher")
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(subscribeUiState.visible) {
|
LaunchedEffect(subscribeUiState.visible) {
|
||||||
@ -180,7 +182,7 @@ fun SubscribeDialog(
|
|||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
launcher.launch("*/*")
|
launcher.launch(arrayOf(MimeType.ANY))
|
||||||
subscribeViewModel.hideDrawer()
|
subscribeViewModel.hideDrawer()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package me.ash.reader.ui.page.home.feeds.subscribe
|
package me.ash.reader.ui.page.home.feeds.subscribe
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.rometools.rome.feed.synd.SyndFeed
|
import com.rometools.rome.feed.synd.SyndFeed
|
||||||
@ -59,12 +58,8 @@ class SubscribeViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun importFromInputStream(inputStream: InputStream) {
|
fun importFromInputStream(inputStream: InputStream) {
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
try {
|
opmlService.saveToDatabase(inputStream)
|
||||||
opmlService.saveToDatabase(inputStream)
|
rssService.get().doSync()
|
||||||
rssService.get().doSync()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("FeedsViewModel", "importFromInputStream: ", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,21 @@
|
|||||||
package me.ash.reader.ui.page.settings
|
package me.ash.reader.ui.page.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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.windowInsetsBottomHeight
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.*
|
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.AccountCircle
|
||||||
|
import androidx.compose.material.icons.outlined.BugReport
|
||||||
|
import androidx.compose.material.icons.outlined.Language
|
||||||
|
import androidx.compose.material.icons.outlined.Lightbulb
|
||||||
|
import androidx.compose.material.icons.outlined.Palette
|
||||||
|
import androidx.compose.material.icons.outlined.TipsAndUpdates
|
||||||
|
import androidx.compose.material.icons.outlined.TouchApp
|
||||||
import androidx.compose.material.icons.rounded.Close
|
import androidx.compose.material.icons.rounded.Close
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -17,7 +28,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.core.os.LocaleListCompat
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
@ -134,6 +144,17 @@ fun SettingsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
SelectableSettingGroupItem(
|
||||||
|
title = stringResource(R.string.troubleshooting),
|
||||||
|
desc = stringResource(R.string.troubleshooting_desc),
|
||||||
|
icon = Icons.Outlined.BugReport,
|
||||||
|
) {
|
||||||
|
navController.navigate(RouteName.TROUBLESHOOTING) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
SelectableSettingGroupItem(
|
SelectableSettingGroupItem(
|
||||||
title = stringResource(R.string.tips_and_support),
|
title = stringResource(R.string.tips_and_support),
|
||||||
|
@ -19,9 +19,9 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.DeleteSweep
|
import androidx.compose.material.icons.outlined.DeleteSweep
|
||||||
import androidx.compose.material.icons.outlined.PersonOff
|
import androidx.compose.material.icons.outlined.PersonOff
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
|
||||||
import androidx.compose.material.icons.rounded.Close
|
import androidx.compose.material.icons.rounded.Close
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -61,6 +61,7 @@ import me.ash.reader.ui.component.base.Subtitle
|
|||||||
import me.ash.reader.ui.component.base.TextFieldDialog
|
import me.ash.reader.ui.component.base.TextFieldDialog
|
||||||
import me.ash.reader.ui.component.base.Tips
|
import me.ash.reader.ui.component.base.Tips
|
||||||
import me.ash.reader.ui.ext.DateFormat
|
import me.ash.reader.ui.ext.DateFormat
|
||||||
|
import me.ash.reader.ui.ext.MimeType
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
import me.ash.reader.ui.ext.getCurrentVersion
|
import me.ash.reader.ui.ext.getCurrentVersion
|
||||||
import me.ash.reader.ui.ext.showToast
|
import me.ash.reader.ui.ext.showToast
|
||||||
@ -103,14 +104,14 @@ fun AccountDetailsPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.CreateDocument("*/*")
|
ActivityResultContracts.CreateDocument(MimeType.ANY)
|
||||||
) { result ->
|
) { result ->
|
||||||
viewModel.exportAsOPML(selectedAccount!!.id!!) { string ->
|
viewModel.exportAsOPML(selectedAccount!!.id!!) { string ->
|
||||||
result?.let { uri ->
|
result?.let { uri ->
|
||||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
outputStream.write(string.toByteArray())
|
outputStream.write(string.toByteArray())
|
||||||
}
|
} ?: context.showToast("Cannot open Input Stream with content resolver")
|
||||||
}
|
} ?: context.showToast("Cannot get activity result with launcher")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,7 +484,7 @@ fun AccountDetailsPage(
|
|||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
exportOPMLModeDialogVisible = false
|
exportOPMLModeDialogVisible = false
|
||||||
launcherOPMLFile(context, launcher)
|
subscriptionOPMLFileLauncher(context, launcher)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.export))
|
Text(stringResource(R.string.export))
|
||||||
@ -502,11 +503,11 @@ fun AccountDetailsPage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launcherOPMLFile(
|
private fun subscriptionOPMLFileLauncher(
|
||||||
context: Context,
|
context: Context,
|
||||||
launcher: ManagedActivityResultLauncher<String, Uri?>,
|
launcher: ManagedActivityResultLauncher<String, Uri?>,
|
||||||
) {
|
) {
|
||||||
launcher.launch("Read-You-" +
|
launcher.launch("Read-You-" +
|
||||||
"${context.getCurrentVersion()}-export-" +
|
"${context.getCurrentVersion()}-subscription-" +
|
||||||
"${Date().toString(DateFormat.YYYY_MM_DD_DASH_HH_MM_SS_DASH)}.opml")
|
"${Date().toString(DateFormat.YYYY_MM_DD_DASH_HH_MM_SS_DASH)}.opml")
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,188 @@
|
|||||||
|
package me.ash.reader.ui.page.settings.troubleshooting
|
||||||
|
|
||||||
|
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.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.automirrored.outlined.KeyboardArrowRight
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material.icons.outlined.ReportGmailerrorred
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.infrastructure.preference.OpenLinkPreference
|
||||||
|
import me.ash.reader.ui.component.base.Banner
|
||||||
|
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.Subtitle
|
||||||
|
import me.ash.reader.ui.ext.DateFormat
|
||||||
|
import me.ash.reader.ui.ext.MimeType
|
||||||
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
|
import me.ash.reader.ui.ext.getCurrentVersion
|
||||||
|
import me.ash.reader.ui.ext.openURL
|
||||||
|
import me.ash.reader.ui.ext.showToast
|
||||||
|
import me.ash.reader.ui.ext.toString
|
||||||
|
import me.ash.reader.ui.page.settings.SettingItem
|
||||||
|
import me.ash.reader.ui.theme.palette.onLight
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TroubleshootingPage(
|
||||||
|
navController: NavHostController,
|
||||||
|
viewModel: TroubleshootingViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val uiState = viewModel.troubleshootingUiState.collectAsStateValue()
|
||||||
|
var byteArray by remember { mutableStateOf(ByteArray(0)) }
|
||||||
|
|
||||||
|
val exportLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument(MimeType.ANY)
|
||||||
|
) { result ->
|
||||||
|
viewModel.exportPreferencesAsJSON(context) { byteArray ->
|
||||||
|
result?.let { uri ->
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
outputStream.write(byteArray)
|
||||||
|
} ?: context.showToast("Cannot open Input Stream with content resolver")
|
||||||
|
} ?: context.showToast("Cannot get activity result with launcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val importLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocument()
|
||||||
|
) {
|
||||||
|
it?.let { uri ->
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
byteArray = inputStream.readBytes()
|
||||||
|
viewModel.tryImport(context, byteArray)
|
||||||
|
} ?: context.showToast("Cannot open Input Stream with content resolver")
|
||||||
|
} ?: context.showToast("Cannot get activity result with launcher")
|
||||||
|
}
|
||||||
|
|
||||||
|
RYScaffold(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
|
navigationIcon = {
|
||||||
|
FeedbackIconButton(
|
||||||
|
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
DisplayText(text = stringResource(R.string.troubleshooting), desc = "")
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Banner(
|
||||||
|
title = stringResource(R.string.bug_report),
|
||||||
|
icon = Icons.Outlined.Info,
|
||||||
|
action = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
|
||||||
|
contentDescription = stringResource(R.string.go_to),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
context.openURL(
|
||||||
|
context.getString(R.string.issue_tracer_url),
|
||||||
|
OpenLinkPreference.AutoPreferCustomTabs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.app_preferences),
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.import_from_protobuf_file),
|
||||||
|
onClick = {
|
||||||
|
importLauncher.launch(arrayOf(MimeType.ANY))
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.export_as_protobuf_file),
|
||||||
|
onClick = {
|
||||||
|
preferenceFileLauncher(context, exportLauncher)
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
RYDialog(
|
||||||
|
visible = uiState.warningDialogVisible,
|
||||||
|
onDismissRequest = { viewModel.hideWarningDialog() },
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.ReportGmailerrorred,
|
||||||
|
contentDescription = stringResource(R.string.import_from_protobuf_file),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.import_from_protobuf_file))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.invalid_protobuf_file_warning))
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.hideWarningDialog()
|
||||||
|
viewModel.importPreferencesFromJSON(context, byteArray)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.confirm))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { viewModel.hideWarningDialog() }) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun preferenceFileLauncher(
|
||||||
|
context: Context,
|
||||||
|
launcher: ManagedActivityResultLauncher<String, Uri?>,
|
||||||
|
) {
|
||||||
|
launcher.launch("Read-You-" +
|
||||||
|
"${context.getCurrentVersion()}-settings-" +
|
||||||
|
"${Date().toString(DateFormat.YYYY_MM_DD_DASH_HH_MM_SS_DASH)}.preferences_pb")
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
package me.ash.reader.ui.page.settings.troubleshooting
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.domain.service.AccountService
|
||||||
|
import me.ash.reader.domain.service.OpmlService
|
||||||
|
import me.ash.reader.domain.service.RssService
|
||||||
|
import me.ash.reader.infrastructure.di.ApplicationScope
|
||||||
|
import me.ash.reader.infrastructure.di.DefaultDispatcher
|
||||||
|
import me.ash.reader.infrastructure.di.IODispatcher
|
||||||
|
import me.ash.reader.infrastructure.di.MainDispatcher
|
||||||
|
import me.ash.reader.ui.ext.getPreferencesFile
|
||||||
|
import me.ash.reader.ui.ext.isProbableProtobuf
|
||||||
|
import me.ash.reader.ui.ext.restart
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class TroubleshootingViewModel @Inject constructor(
|
||||||
|
private val accountService: AccountService,
|
||||||
|
private val rssService: RssService,
|
||||||
|
private val opmlService: OpmlService,
|
||||||
|
@IODispatcher
|
||||||
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
@DefaultDispatcher
|
||||||
|
private val defaultDispatcher: CoroutineDispatcher,
|
||||||
|
@MainDispatcher
|
||||||
|
private val mainDispatcher: CoroutineDispatcher,
|
||||||
|
@ApplicationScope
|
||||||
|
private val applicationScope: CoroutineScope,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _troubleshootingUiState = MutableStateFlow(TroubleshootingUiState())
|
||||||
|
val troubleshootingUiState: StateFlow<TroubleshootingUiState> =
|
||||||
|
_troubleshootingUiState.asStateFlow()
|
||||||
|
|
||||||
|
fun showWarningDialog() {
|
||||||
|
_troubleshootingUiState.update { it.copy(warningDialogVisible = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideWarningDialog() {
|
||||||
|
_troubleshootingUiState.update { it.copy(warningDialogVisible = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tryImport(context: Context, byteArray: ByteArray) {
|
||||||
|
if (!byteArray.isProbableProtobuf()) {
|
||||||
|
showWarningDialog()
|
||||||
|
} else {
|
||||||
|
importPreferencesFromJSON(context, byteArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importPreferencesFromJSON(context: Context, byteArray: ByteArray) {
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
val file = context.getPreferencesFile()
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
if (file.createNewFile()) file.writeBytes(byteArray)
|
||||||
|
context.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportPreferencesAsJSON(context: Context, callback: (ByteArray) -> Unit = {}) {
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
val file = context.getPreferencesFile()
|
||||||
|
callback(if (file.exists()) file.readBytes() else byteArrayOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TroubleshootingUiState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val warningDialogVisible: Boolean = false,
|
||||||
|
)
|
@ -410,7 +410,7 @@
|
|||||||
<string name="grey_out_articles">Grey out articles</string>
|
<string name="grey_out_articles">Grey out articles</string>
|
||||||
<string name="all_read">All read</string>
|
<string name="all_read">All read</string>
|
||||||
<string name="read_excluding_starred">Read, excluding starred</string>
|
<string name="read_excluding_starred">Read, excluding starred</string>
|
||||||
<string name="external_links">External links</string>
|
<string name="external_links">External Links</string>
|
||||||
<string name="unexpected_error_title">Oops! Something went wrong…</string>
|
<string name="unexpected_error_title">Oops! Something went wrong…</string>
|
||||||
<string name="copy_error_report">Copy error report</string>
|
<string name="copy_error_report">Copy error report</string>
|
||||||
<string name="submit_bug_report">submit a bug report on GitHub</string>
|
<string name="submit_bug_report">submit a bug report on GitHub</string>
|
||||||
@ -432,4 +432,12 @@
|
|||||||
<string name="shared_content">Shared content</string>
|
<string name="shared_content">Shared content</string>
|
||||||
<string name="only_link">Only link</string>
|
<string name="only_link">Only link</string>
|
||||||
<string name="title_and_link">Title and link</string>
|
<string name="title_and_link">Title and link</string>
|
||||||
|
<string name="troubleshooting">Troubleshooting</string>
|
||||||
|
<string name="troubleshooting_desc">Bug report, debug tools</string>
|
||||||
|
<string name="bug_report">Bug report</string>
|
||||||
|
<string name="issue_tracer_url" translatable="false">https://github.com/Ashinch/ReadYou/issues</string>
|
||||||
|
<string name="app_preferences">App preferences</string>
|
||||||
|
<string name="import_from_protobuf_file">Import from protobuf file</string>
|
||||||
|
<string name="export_as_protobuf_file">Export as protobuf file</string>
|
||||||
|
<string name="invalid_protobuf_file_warning">This file may not be a valid protobuf file. Importing it could potentially corrupt the app and result in the loss of current preferences. Are you sure you want to proceed?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user