Refactor backup option flags to normal data class of booleans
This commit is contained in:
parent
8b65fd5751
commit
8735836498
7 changed files with 133 additions and 83 deletions
|
@ -29,16 +29,11 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.PersistentSet
|
|
||||||
import kotlinx.collections.immutable.minus
|
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
|
||||||
import kotlinx.collections.immutable.plus
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
|
@ -101,13 +96,13 @@ class CreateBackupScreen : Screen() {
|
||||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BackupChoices.forEach { (k, v) ->
|
BackupOptions.entries.forEach { option ->
|
||||||
item {
|
item {
|
||||||
LabeledCheckbox(
|
LabeledCheckbox(
|
||||||
label = stringResource(v),
|
label = stringResource(option.label),
|
||||||
checked = state.flags.contains(k),
|
checked = option.getter(state.options),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
model.toggleFlag(k)
|
model.toggle(option.setter, it)
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
)
|
)
|
||||||
|
@ -145,37 +140,28 @@ class CreateBackupScreen : Screen() {
|
||||||
|
|
||||||
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
|
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
|
||||||
|
|
||||||
fun toggleFlag(flag: Int) {
|
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
if (it.flags.contains(flag)) {
|
it.copy(
|
||||||
it.copy(flags = it.flags - flag)
|
options = setter(it.options, enabled),
|
||||||
} else {
|
)
|
||||||
it.copy(flags = it.flags + flag)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createBackup(context: Context, uri: Uri) {
|
fun createBackup(context: Context, uri: Uri) {
|
||||||
val flags = state.value.flags.fold(initial = 0, operation = { a, b -> a or b })
|
BackupCreateJob.startNow(context, uri, state.value.options)
|
||||||
BackupCreateJob.startNow(context, uri, flags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val flags: PersistentSet<Int> = persistentSetOf(
|
val options: BackupOptions = BackupOptions(
|
||||||
BackupCreateFlags.BACKUP_CATEGORY,
|
libraryEntries = true,
|
||||||
BackupCreateFlags.BACKUP_CHAPTER,
|
categories = true,
|
||||||
BackupCreateFlags.BACKUP_TRACK,
|
chapters = true,
|
||||||
BackupCreateFlags.BACKUP_HISTORY,
|
tracking = true,
|
||||||
|
history = true,
|
||||||
|
appSettings = false,
|
||||||
|
sourceSettings = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val BackupChoices = persistentMapOf(
|
|
||||||
BackupCreateFlags.BACKUP_CATEGORY to MR.strings.categories,
|
|
||||||
BackupCreateFlags.BACKUP_CHAPTER to MR.strings.chapters,
|
|
||||||
BackupCreateFlags.BACKUP_TRACK to MR.strings.track,
|
|
||||||
BackupCreateFlags.BACKUP_HISTORY to MR.strings.history,
|
|
||||||
BackupCreateFlags.BACKUP_APP_PREFS to MR.strings.app_settings,
|
|
||||||
BackupCreateFlags.BACKUP_SOURCE_PREFS to MR.strings.source_settings,
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.data.backup.create
|
|
||||||
|
|
||||||
internal object BackupCreateFlags {
|
|
||||||
const val BACKUP_CATEGORY = 0x1
|
|
||||||
const val BACKUP_CHAPTER = 0x2
|
|
||||||
const val BACKUP_HISTORY = 0x4
|
|
||||||
const val BACKUP_TRACK = 0x8
|
|
||||||
const val BACKUP_APP_PREFS = 0x10
|
|
||||||
const val BACKUP_SOURCE_PREFS = 0x20
|
|
||||||
|
|
||||||
const val AutomaticDefaults = BACKUP_CATEGORY or
|
|
||||||
BACKUP_CHAPTER or
|
|
||||||
BACKUP_HISTORY or
|
|
||||||
BACKUP_TRACK or
|
|
||||||
BACKUP_APP_PREFS or
|
|
||||||
BACKUP_SOURCE_PREFS
|
|
||||||
}
|
|
|
@ -47,10 +47,12 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||||
|
|
||||||
setForegroundSafely()
|
setForegroundSafely()
|
||||||
|
|
||||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
|
val options = inputData.getBooleanArray(OPTIONS_KEY)
|
||||||
|
?.let { BackupOptions.fromBooleanArray(it) }
|
||||||
|
?: BackupOptions.AutomaticDefaults
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val location = BackupCreator(context, isAutoBackup).backup(uri, flags)
|
val location = BackupCreator(context, isAutoBackup).backup(uri, options)
|
||||||
if (!isAutoBackup) {
|
if (!isAutoBackup) {
|
||||||
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
|
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
|
||||||
}
|
}
|
||||||
|
@ -112,11 +114,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startNow(context: Context, uri: Uri, flags: Int) {
|
fun startNow(context: Context, uri: Uri, options: BackupOptions) {
|
||||||
val inputData = workDataOf(
|
val inputData = workDataOf(
|
||||||
IS_AUTO_BACKUP_KEY to false,
|
IS_AUTO_BACKUP_KEY to false,
|
||||||
LOCATION_URI_KEY to uri.toString(),
|
LOCATION_URI_KEY to uri.toString(),
|
||||||
BACKUP_FLAGS_KEY to flags,
|
OPTIONS_KEY to options.toBooleanArray(),
|
||||||
)
|
)
|
||||||
val request = OneTimeWorkRequestBuilder<BackupCreateJob>()
|
val request = OneTimeWorkRequestBuilder<BackupCreateJob>()
|
||||||
.addTag(TAG_MANUAL)
|
.addTag(TAG_MANUAL)
|
||||||
|
@ -132,4 +134,4 @@ private const val TAG_MANUAL = "$TAG_AUTO:manual"
|
||||||
|
|
||||||
private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
|
private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
|
||||||
private const val LOCATION_URI_KEY = "location_uri" // String
|
private const val LOCATION_URI_KEY = "location_uri" // String
|
||||||
private const val BACKUP_FLAGS_KEY = "backup_flags" // Int
|
private const val OPTIONS_KEY = "options" // BooleanArray
|
||||||
|
|
|
@ -5,9 +5,6 @@ import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
||||||
|
@ -52,7 +49,7 @@ class BackupCreator(
|
||||||
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun backup(uri: Uri, flags: Int): String {
|
suspend fun backup(uri: Uri, options: BackupOptions): String {
|
||||||
var file: UniFile? = null
|
var file: UniFile? = null
|
||||||
try {
|
try {
|
||||||
file = (
|
file = (
|
||||||
|
@ -80,11 +77,11 @@ class BackupCreator(
|
||||||
|
|
||||||
val databaseManga = getFavorites.await()
|
val databaseManga = getFavorites.await()
|
||||||
val backup = Backup(
|
val backup = Backup(
|
||||||
backupManga = backupMangas(databaseManga, flags),
|
backupManga = backupMangas(databaseManga, options),
|
||||||
backupCategories = backupCategories(flags),
|
backupCategories = backupCategories(options),
|
||||||
backupSources = backupSources(databaseManga),
|
backupSources = backupSources(databaseManga),
|
||||||
backupPreferences = backupAppPreferences(flags),
|
backupPreferences = backupAppPreferences(options),
|
||||||
backupSourcePreferences = backupSourcePreferences(flags),
|
backupSourcePreferences = backupSourcePreferences(options),
|
||||||
)
|
)
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||||
|
@ -117,28 +114,28 @@ class BackupCreator(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun backupCategories(options: Int): List<BackupCategory> {
|
private suspend fun backupCategories(options: BackupOptions): List<BackupCategory> {
|
||||||
if (options and BACKUP_CATEGORY != BACKUP_CATEGORY) return emptyList()
|
if (!options.categories) return emptyList()
|
||||||
|
|
||||||
return categoriesBackupCreator.backupCategories()
|
return categoriesBackupCreator.backupCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
private suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||||
return mangaBackupCreator.backupMangas(mangas, flags)
|
return mangaBackupCreator.backupMangas(mangas, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
private fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
||||||
return sourcesBackupCreator.backupSources(mangas)
|
return sourcesBackupCreator.backupSources(mangas)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backupAppPreferences(flags: Int): List<BackupPreference> {
|
private fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||||
if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList()
|
if (!options.appSettings) return emptyList()
|
||||||
|
|
||||||
return preferenceBackupCreator.backupAppPreferences()
|
return preferenceBackupCreator.backupAppPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
|
private fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||||
if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList()
|
if (!options.sourceSettings) return emptyList()
|
||||||
|
|
||||||
return preferenceBackupCreator.backupSourcePreferences()
|
return preferenceBackupCreator.backupSourcePreferences()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package eu.kanade.tachiyomi.data.backup.create
|
||||||
|
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
|
data class BackupOptions(
|
||||||
|
val libraryEntries: Boolean = true,
|
||||||
|
val categories: Boolean = true,
|
||||||
|
val chapters: Boolean = true,
|
||||||
|
val tracking: Boolean = true,
|
||||||
|
val history: Boolean = true,
|
||||||
|
val appSettings: Boolean = true,
|
||||||
|
val sourceSettings: Boolean = true,
|
||||||
|
) {
|
||||||
|
fun toBooleanArray() = booleanArrayOf(
|
||||||
|
libraryEntries,
|
||||||
|
categories,
|
||||||
|
chapters,
|
||||||
|
tracking,
|
||||||
|
history,
|
||||||
|
appSettings,
|
||||||
|
sourceSettings,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val AutomaticDefaults = BackupOptions(
|
||||||
|
libraryEntries = true,
|
||||||
|
categories = true,
|
||||||
|
chapters = true,
|
||||||
|
tracking = true,
|
||||||
|
history = true,
|
||||||
|
appSettings = true,
|
||||||
|
sourceSettings = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun fromBooleanArray(booleanArray: BooleanArray) = BackupOptions(
|
||||||
|
libraryEntries = booleanArray[0],
|
||||||
|
categories = booleanArray[1],
|
||||||
|
chapters = booleanArray[2],
|
||||||
|
tracking = booleanArray[3],
|
||||||
|
history = booleanArray[4],
|
||||||
|
appSettings = booleanArray[5],
|
||||||
|
sourceSettings = booleanArray[6],
|
||||||
|
)
|
||||||
|
|
||||||
|
val entries = persistentListOf<BackupOptionEntry>(
|
||||||
|
BackupOptionEntry(
|
||||||
|
label = MR.strings.categories,
|
||||||
|
getter = BackupOptions::categories,
|
||||||
|
setter = { options, enabled -> options.copy(categories = enabled) },
|
||||||
|
),
|
||||||
|
BackupOptionEntry(
|
||||||
|
label = MR.strings.chapters,
|
||||||
|
getter = BackupOptions::chapters,
|
||||||
|
setter = { options, enabled -> options.copy(chapters = enabled) },
|
||||||
|
),
|
||||||
|
BackupOptionEntry(
|
||||||
|
label = MR.strings.track,
|
||||||
|
getter = BackupOptions::tracking,
|
||||||
|
setter = { options, enabled -> options.copy(tracking = enabled) },
|
||||||
|
),
|
||||||
|
BackupOptionEntry(
|
||||||
|
label = MR.strings.history,
|
||||||
|
getter = BackupOptions::history,
|
||||||
|
setter = { options, enabled -> options.copy(history = enabled) },
|
||||||
|
),
|
||||||
|
BackupOptionEntry(
|
||||||
|
label = MR.strings.app_settings,
|
||||||
|
getter = BackupOptions::appSettings,
|
||||||
|
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||||
|
),
|
||||||
|
BackupOptionEntry(
|
||||||
|
label = MR.strings.source_settings,
|
||||||
|
getter = BackupOptions::sourceSettings,
|
||||||
|
setter = { options, enabled -> options.copy(sourceSettings = enabled) },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BackupOptionEntry(
|
||||||
|
val label: StringResource,
|
||||||
|
val getter: (BackupOptions) -> Boolean,
|
||||||
|
val setter: (BackupOptions, Boolean) -> BackupOptions,
|
||||||
|
)
|
|
@ -1,6 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.data.backup.create.creators
|
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||||
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
|
||||||
|
@ -20,18 +20,17 @@ class MangaBackupCreator(
|
||||||
private val getHistory: GetHistory = Injekt.get(),
|
private val getHistory: GetHistory = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||||
return mangas.map {
|
return mangas.map {
|
||||||
backupManga(it, flags)
|
backupManga(it, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
|
private suspend fun backupManga(manga: Manga, options: BackupOptions): BackupManga {
|
||||||
// Entry for this manga
|
// Entry for this manga
|
||||||
val mangaObject = manga.toBackupManga()
|
val mangaObject = manga.toBackupManga()
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
if (options.chapters) {
|
||||||
if (options and BackupCreateFlags.BACKUP_CHAPTER == BackupCreateFlags.BACKUP_CHAPTER) {
|
|
||||||
// Backup all the chapters
|
// Backup all the chapters
|
||||||
handler.awaitList {
|
handler.awaitList {
|
||||||
chaptersQueries.getChaptersByMangaId(
|
chaptersQueries.getChaptersByMangaId(
|
||||||
|
@ -44,8 +43,7 @@ class MangaBackupCreator(
|
||||||
?.let { mangaObject.chapters = it }
|
?.let { mangaObject.chapters = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
if (options.categories) {
|
||||||
if (options and BackupCreateFlags.BACKUP_CATEGORY == BackupCreateFlags.BACKUP_CATEGORY) {
|
|
||||||
// Backup categories for this manga
|
// Backup categories for this manga
|
||||||
val categoriesForManga = getCategories.await(manga.id)
|
val categoriesForManga = getCategories.await(manga.id)
|
||||||
if (categoriesForManga.isNotEmpty()) {
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
|
@ -53,16 +51,14 @@ class MangaBackupCreator(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
if (options.tracking) {
|
||||||
if (options and BackupCreateFlags.BACKUP_TRACK == BackupCreateFlags.BACKUP_TRACK) {
|
|
||||||
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
||||||
if (tracks.isNotEmpty()) {
|
if (tracks.isNotEmpty()) {
|
||||||
mangaObject.tracking = tracks
|
mangaObject.tracking = tracks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user wants history information in backup
|
if (options.history) {
|
||||||
if (options and BackupCreateFlags.BACKUP_HISTORY == BackupCreateFlags.BACKUP_HISTORY) {
|
|
||||||
val historyByMangaId = getHistory.await(manga.id)
|
val historyByMangaId = getHistory.await(manga.id)
|
||||||
if (historyByMangaId.isNotEmpty()) {
|
if (historyByMangaId.isNotEmpty()) {
|
||||||
val history = historyByMangaId.map { history ->
|
val history = historyByMangaId.map { history ->
|
||||||
|
|
|
@ -40,7 +40,7 @@ class PreferenceBackupCreator(
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
||||||
return this.filterKeys {
|
return this.filterKeys {
|
||||||
!Preference.isPrivate(it) && !Preference.isAppState(it)
|
!Preference.isAppState(it) && !Preference.isPrivate(it)
|
||||||
}
|
}
|
||||||
.mapNotNull { (key, value) ->
|
.mapNotNull { (key, value) ->
|
||||||
when (value) {
|
when (value) {
|
||||||
|
|
Reference in a new issue