mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-21 20:47:03 -05:00
Add app settings to backups
This should be compatible with Aniyomi's implementation. Related to #1857 Co-authored-by: jmir1 <jmir1@users.noreply.github.com>
This commit is contained in:
parent
9c688b08c0
commit
72024aa44a
11 changed files with 141 additions and 9 deletions
|
@ -147,6 +147,7 @@ object SettingsBackupScreen : SearchableSettings {
|
||||||
BackupConst.BACKUP_CHAPTER to R.string.chapters,
|
BackupConst.BACKUP_CHAPTER to R.string.chapters,
|
||||||
BackupConst.BACKUP_TRACK to R.string.track,
|
BackupConst.BACKUP_TRACK to R.string.track,
|
||||||
BackupConst.BACKUP_HISTORY to R.string.history,
|
BackupConst.BACKUP_HISTORY to R.string.history,
|
||||||
|
BackupConst.BACKUP_APP_PREFS to R.string.app_settings,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val flags = remember { choices.keys.toMutableStateList() }
|
val flags = remember { choices.keys.toMutableStateList() }
|
||||||
|
|
|
@ -4,11 +4,18 @@ package eu.kanade.tachiyomi.data.backup
|
||||||
internal object BackupConst {
|
internal object BackupConst {
|
||||||
const val BACKUP_CATEGORY = 0x1
|
const val BACKUP_CATEGORY = 0x1
|
||||||
const val BACKUP_CATEGORY_MASK = 0x1
|
const val BACKUP_CATEGORY_MASK = 0x1
|
||||||
|
|
||||||
const val BACKUP_CHAPTER = 0x2
|
const val BACKUP_CHAPTER = 0x2
|
||||||
const val BACKUP_CHAPTER_MASK = 0x2
|
const val BACKUP_CHAPTER_MASK = 0x2
|
||||||
|
|
||||||
const val BACKUP_HISTORY = 0x4
|
const val BACKUP_HISTORY = 0x4
|
||||||
const val BACKUP_HISTORY_MASK = 0x4
|
const val BACKUP_HISTORY_MASK = 0x4
|
||||||
|
|
||||||
const val BACKUP_TRACK = 0x8
|
const val BACKUP_TRACK = 0x8
|
||||||
const val BACKUP_TRACK_MASK = 0x8
|
const val BACKUP_TRACK_MASK = 0x8
|
||||||
const val BACKUP_ALL = 0xF
|
|
||||||
|
const val BACKUP_APP_PREFS = 0x10
|
||||||
|
const val BACKUP_APP_PREFS_MASK = 0x10
|
||||||
|
|
||||||
|
const val BACKUP_ALL = 0x1F
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.domain.chapter.model.copyFrom
|
import eu.kanade.domain.chapter.model.copyFrom
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
||||||
|
@ -18,8 +20,15 @@ import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
|
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
||||||
|
@ -30,6 +39,7 @@ import logcat.LogPriority
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
||||||
import tachiyomi.data.Manga_sync
|
import tachiyomi.data.Manga_sync
|
||||||
|
@ -61,6 +71,7 @@ class BackupManager(
|
||||||
private val getCategories: GetCategories = Injekt.get()
|
private val getCategories: GetCategories = Injekt.get()
|
||||||
private val getFavorites: GetFavorites = Injekt.get()
|
private val getFavorites: GetFavorites = Injekt.get()
|
||||||
private val getHistory: GetHistory = Injekt.get()
|
private val getHistory: GetHistory = Injekt.get()
|
||||||
|
private val preferenceStore: PreferenceStore = Injekt.get()
|
||||||
|
|
||||||
internal val parser = ProtoBuf
|
internal val parser = ProtoBuf
|
||||||
|
|
||||||
|
@ -81,6 +92,7 @@ class BackupManager(
|
||||||
backupCategories(flags),
|
backupCategories(flags),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
prepExtensionInfoForSync(databaseManga),
|
prepExtensionInfoForSync(databaseManga),
|
||||||
|
backupAppPreferences(flags),
|
||||||
)
|
)
|
||||||
|
|
||||||
var file: UniFile? = null
|
var file: UniFile? = null
|
||||||
|
@ -133,7 +145,7 @@ class BackupManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
|
private fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
|
||||||
return mangas
|
return mangas
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.map(Manga::source)
|
.map(Manga::source)
|
||||||
|
@ -148,7 +160,7 @@ class BackupManager(
|
||||||
*
|
*
|
||||||
* @return list of [BackupCategory] to be backed up
|
* @return list of [BackupCategory] to be backed up
|
||||||
*/
|
*/
|
||||||
suspend fun backupCategories(options: Int): List<BackupCategory> {
|
private suspend fun backupCategories(options: Int): List<BackupCategory> {
|
||||||
// Check if user wants category information in backup
|
// Check if user wants category information in backup
|
||||||
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
getCategories.await()
|
getCategories.await()
|
||||||
|
@ -159,7 +171,7 @@ class BackupManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||||
return mangas.map {
|
return mangas.map {
|
||||||
backupManga(it, flags)
|
backupManga(it, flags)
|
||||||
}
|
}
|
||||||
|
@ -219,6 +231,25 @@ class BackupManager(
|
||||||
return mangaObject
|
return mangaObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun backupAppPreferences(flags: Int): List<BackupPreference> {
|
||||||
|
if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
|
||||||
|
|
||||||
|
return preferenceStore.getAll().mapNotNull { (key, value) ->
|
||||||
|
when (value) {
|
||||||
|
is Int -> BackupPreference(key, IntPreferenceValue(value))
|
||||||
|
is Long -> BackupPreference(key, LongPreferenceValue(value))
|
||||||
|
is Float -> BackupPreference(key, FloatPreferenceValue(value))
|
||||||
|
is String -> BackupPreference(key, StringPreferenceValue(value))
|
||||||
|
is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
|
||||||
|
is Set<*> -> (value as? Set<String>)?.let {
|
||||||
|
BackupPreference(key, StringSetPreferenceValue(it))
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
||||||
var updatedManga = manga.copy(id = dbManga._id)
|
var updatedManga = manga.copy(id = dbManga._id)
|
||||||
updatedManga = updatedManga.copyFrom(dbManga)
|
updatedManga = updatedManga.copyFrom(dbManga)
|
||||||
|
|
|
@ -7,13 +7,20 @@ import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
||||||
import eu.kanade.tachiyomi.util.BackupUtil
|
import eu.kanade.tachiyomi.util.BackupUtil
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
|
||||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
|
@ -30,8 +37,8 @@ class BackupRestorer(
|
||||||
private val notifier: BackupNotifier,
|
private val notifier: BackupNotifier,
|
||||||
) {
|
) {
|
||||||
private val updateManga: UpdateManga = Injekt.get()
|
private val updateManga: UpdateManga = Injekt.get()
|
||||||
private val chapterRepository: ChapterRepository = Injekt.get()
|
|
||||||
private val fetchInterval: FetchInterval = Injekt.get()
|
private val fetchInterval: FetchInterval = Injekt.get()
|
||||||
|
private val preferenceStore: PreferenceStore = Injekt.get()
|
||||||
|
|
||||||
private var now = ZonedDateTime.now()
|
private var now = ZonedDateTime.now()
|
||||||
private var currentFetchWindow = fetchInterval.getWindow(now)
|
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||||
|
@ -106,6 +113,8 @@ class BackupRestorer(
|
||||||
currentFetchWindow = fetchInterval.getWindow(now)
|
currentFetchWindow = fetchInterval.getWindow(now)
|
||||||
|
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
|
restoreAppPreferences(backup.backupPreferences)
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
backup.backupManga.forEach {
|
backup.backupManga.forEach {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
|
@ -115,6 +124,7 @@ class BackupRestorer(
|
||||||
restoreManga(it, backup.backupCategories, sync)
|
restoreManga(it, backup.backupCategories, sync)
|
||||||
}
|
}
|
||||||
// TODO: optionally trigger online library + tracker update
|
// TODO: optionally trigger online library + tracker update
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,6 +210,45 @@ class BackupRestorer(
|
||||||
backupManager.restoreTracking(manga, tracks)
|
backupManager.restoreTracking(manga, tracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun restoreAppPreferences(preferences: List<BackupPreference>) {
|
||||||
|
val prefs = preferenceStore.getAll()
|
||||||
|
|
||||||
|
preferences.forEach { (key, value) ->
|
||||||
|
when (value) {
|
||||||
|
is IntPreferenceValue -> {
|
||||||
|
if (prefs[key] is Int?) {
|
||||||
|
preferenceStore.getInt(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LongPreferenceValue -> {
|
||||||
|
if (prefs[key] is Long?) {
|
||||||
|
preferenceStore.getLong(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FloatPreferenceValue -> {
|
||||||
|
if (prefs[key] is Float?) {
|
||||||
|
preferenceStore.getFloat(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is StringPreferenceValue -> {
|
||||||
|
if (prefs[key] is String?) {
|
||||||
|
preferenceStore.getString(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is BooleanPreferenceValue -> {
|
||||||
|
if (prefs[key] is Boolean?) {
|
||||||
|
preferenceStore.getBoolean(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is StringSetPreferenceValue -> {
|
||||||
|
if (prefs[key] is Set<*>?) {
|
||||||
|
preferenceStore.getStringSet(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called to update dialog in [BackupConst]
|
* Called to update dialog in [BackupConst]
|
||||||
*
|
*
|
||||||
|
|
|
@ -11,9 +11,9 @@ import java.util.Locale
|
||||||
data class Backup(
|
data class Backup(
|
||||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||||
// Bump by 100 to specify this is a 0.x value
|
|
||||||
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||||
|
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -9,7 +9,6 @@ class BackupCategory(
|
||||||
@ProtoNumber(1) var name: String,
|
@ProtoNumber(1) var name: String,
|
||||||
@ProtoNumber(2) var order: Long = 0,
|
@ProtoNumber(2) var order: Long = 0,
|
||||||
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||||
// Bump by 100 to specify this is a 0.x value
|
|
||||||
@ProtoNumber(100) var flags: Long = 0,
|
@ProtoNumber(100) var flags: Long = 0,
|
||||||
) {
|
) {
|
||||||
fun getCategory(): Category {
|
fun getCategory(): Category {
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupPreference(
|
||||||
|
@ProtoNumber(1) val key: String,
|
||||||
|
@ProtoNumber(2) val value: PreferenceValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class PreferenceValue
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class IntPreferenceValue(val value: Int) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LongPreferenceValue(val value: Long) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FloatPreferenceValue(val value: Float) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StringPreferenceValue(val value: String) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BooleanPreferenceValue(val value: Boolean) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StringSetPreferenceValue(val value: Set<String>) : PreferenceValue()
|
|
@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
|
||||||
sealed class AndroidPreference<T>(
|
sealed class AndroidPreference<T>(
|
||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
|
@ -29,7 +30,13 @@ sealed class AndroidPreference<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(): T {
|
override fun get(): T {
|
||||||
return read(preferences, key, defaultValue)
|
return try {
|
||||||
|
read(preferences, key, defaultValue)
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
logcat { "Invalid value for $key; deleting" }
|
||||||
|
delete()
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun set(value: T) {
|
override fun set(value: T) {
|
||||||
|
|
|
@ -60,6 +60,10 @@ class AndroidPreferenceStore(
|
||||||
deserializer = deserializer,
|
deserializer = deserializer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getAll(): Map<String, *> {
|
||||||
|
return sharedPreferences.all ?: emptyMap<String, Any>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val SharedPreferences.keyFlow
|
private val SharedPreferences.keyFlow
|
||||||
|
|
|
@ -20,6 +20,8 @@ interface PreferenceStore {
|
||||||
serializer: (T) -> String,
|
serializer: (T) -> String,
|
||||||
deserializer: (String) -> T,
|
deserializer: (String) -> T,
|
||||||
): Preference<T>
|
): Preference<T>
|
||||||
|
|
||||||
|
fun getAll(): Map<String, *>
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
||||||
|
|
|
@ -493,6 +493,7 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="backup_in_progress">Backup is already in progress</string>
|
<string name="backup_in_progress">Backup is already in progress</string>
|
||||||
<string name="backup_choice">What do you want to backup?</string>
|
<string name="backup_choice">What do you want to backup?</string>
|
||||||
|
<string name="app_settings">App settings</string>
|
||||||
<string name="creating_backup">Creating backup</string>
|
<string name="creating_backup">Creating backup</string>
|
||||||
<string name="creating_backup_error">Backup failed</string>
|
<string name="creating_backup_error">Backup failed</string>
|
||||||
<string name="missing_storage_permission">Storage permissions not granted</string>
|
<string name="missing_storage_permission">Storage permissions not granted</string>
|
||||||
|
|
Loading…
Reference in a new issue