From 23f8f35354b30aded0749222234f76a4f9eaab5c Mon Sep 17 00:00:00 2001 From: FourTOne5 <59261191+FourTOne5@users.noreply.github.com> Date: Sat, 30 Apr 2022 08:49:02 -0700 Subject: [PATCH] Migrate Source Filter Screen to Compose (#7031) * Migrate Source Filter Screen to Compose * Changes from Review and some more fixes * Rename some variable and classes * Review Change * Ewbase and Review changes --- .../data/source/SourceRepositoryImpl.kt | 6 + .../java/eu/kanade/domain/DomainModule.kt | 8 +- .../domain/source/interactor/DisableSource.kt | 14 -- .../interactor/GetLanguagesWithSources.kt | 35 +++++ .../source/interactor/ToggleLanguage.kt | 19 +++ .../domain/source/interactor/ToggleSource.kt | 20 +++ .../source/repository/SourceRepository.kt | 2 + .../presentation/source/SourceFilterScreen.kt | 130 ++++++++++++++++++ .../kanade/tachiyomi/source/SourceManager.kt | 3 + .../ui/browse/source/SourceController.kt | 2 +- .../browse/source/SourceFilterController.kt | 126 ++++------------- .../ui/browse/source/SourceFilterPresenter.kt | 71 ++++++++++ .../ui/browse/source/SourcePresenter.kt | 8 +- app/src/main/res/values/strings.xml | 3 + 14 files changed, 324 insertions(+), 123 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/DisableSource.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/GetLanguagesWithSources.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/ToggleLanguage.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt create mode 100644 app/src/main/java/eu/kanade/presentation/source/SourceFilterScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt diff --git a/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt index 52d826fc61..1dddc01e16 100644 --- a/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt @@ -19,6 +19,12 @@ class SourceRepositoryImpl( } } + override fun getOnlineSources(): Flow> { + return sourceManager.onlineSources.map { sources -> + sources.map(sourceMapper) + } + } + override fun getSourcesWithFavoriteCount(): Flow>> { val sourceIdWithFavoriteCount = handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() } return sourceIdWithFavoriteCount.map { sourceIdsWithCount -> diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 95f54676db..ee5668767d 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -11,10 +11,12 @@ import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId import eu.kanade.domain.manga.repository.MangaRepository -import eu.kanade.domain.source.interactor.DisableSource import eu.kanade.domain.source.interactor.GetEnabledSources +import eu.kanade.domain.source.interactor.GetLanguagesWithSources import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.SetMigrateSorting +import eu.kanade.domain.source.interactor.ToggleLanguage +import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.repository.SourceRepository import uy.kohesive.injekt.api.InjektModule @@ -37,10 +39,12 @@ class DomainModule : InjektModule { addFactory { RemoveHistoryByMangaId(get()) } addSingletonFactory { SourceRepositoryImpl(get(), get()) } + addFactory { GetLanguagesWithSources(get(), get()) } addFactory { GetEnabledSources(get(), get()) } - addFactory { DisableSource(get()) } + addFactory { ToggleSource(get()) } addFactory { ToggleSourcePin(get()) } addFactory { GetSourcesWithFavoriteCount(get(), get()) } addFactory { SetMigrateSorting(get()) } + addFactory { ToggleLanguage(get()) } } } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/DisableSource.kt b/app/src/main/java/eu/kanade/domain/source/interactor/DisableSource.kt deleted file mode 100644 index 336be8b99a..0000000000 --- a/app/src/main/java/eu/kanade/domain/source/interactor/DisableSource.kt +++ /dev/null @@ -1,14 +0,0 @@ -package eu.kanade.domain.source.interactor - -import eu.kanade.domain.source.model.Source -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.util.preference.plusAssign - -class DisableSource( - private val preferences: PreferencesHelper -) { - - fun await(source: Source) { - preferences.disabledSources() += source.id.toString() - } -} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetLanguagesWithSources.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetLanguagesWithSources.kt new file mode 100644 index 0000000000..1c157b5c11 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetLanguagesWithSources.kt @@ -0,0 +1,35 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.domain.source.model.Source +import eu.kanade.domain.source.repository.SourceRepository +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.system.LocaleHelper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +class GetLanguagesWithSources( + private val repository: SourceRepository, + private val preferences: PreferencesHelper, +) { + + fun subscribe(): Flow>> { + return combine( + preferences.enabledLanguages().asFlow(), + preferences.disabledSources().asFlow(), + repository.getOnlineSources() + ) { enabledLanguage, disabledSource, onlineSources -> + val sortedSources = onlineSources.sortedWith( + compareBy { it.id.toString() in disabledSource } + .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name } + ) + + sortedSources.groupBy { it.lang } + .toSortedMap( + compareBy( + { it !in enabledLanguage }, + { LocaleHelper.getDisplayName(it) } + ) + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleLanguage.kt b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleLanguage.kt new file mode 100644 index 0000000000..da373e829e --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleLanguage.kt @@ -0,0 +1,19 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.minusAssign +import eu.kanade.tachiyomi.util.preference.plusAssign + +class ToggleLanguage( + val preferences: PreferencesHelper +) { + + fun await(language: String) { + val isEnabled = language in preferences.enabledLanguages().get() + if (isEnabled) { + preferences.enabledLanguages() -= language + } else { + preferences.enabledLanguages() += language + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt new file mode 100644 index 0000000000..8c9296d129 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt @@ -0,0 +1,20 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.domain.source.model.Source +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.minusAssign +import eu.kanade.tachiyomi.util.preference.plusAssign + +class ToggleSource( + private val preferences: PreferencesHelper +) { + + fun await(source: Source) { + val isEnabled = source.id.toString() !in preferences.disabledSources().get() + if (isEnabled) { + preferences.disabledSources() += source.id.toString() + } else { + preferences.disabledSources() -= source.id.toString() + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt b/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt index b58509de9f..5f07a30f5b 100644 --- a/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt +++ b/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt @@ -7,5 +7,7 @@ interface SourceRepository { fun getSources(): Flow> + fun getOnlineSources(): Flow> + fun getSourcesWithFavoriteCount(): Flow>> } diff --git a/app/src/main/java/eu/kanade/presentation/source/SourceFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/source/SourceFilterScreen.kt new file mode 100644 index 0000000000..d300ad5079 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/source/SourceFilterScreen.kt @@ -0,0 +1,130 @@ +package eu.kanade.presentation.source + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import eu.kanade.domain.source.model.Source +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.LoadingScreen +import eu.kanade.presentation.components.PreferenceRow +import eu.kanade.presentation.source.components.BaseSourceItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel +import eu.kanade.tachiyomi.ui.browse.source.SourceFilterPresenter +import eu.kanade.tachiyomi.ui.browse.source.SourceFilterState +import eu.kanade.tachiyomi.util.system.LocaleHelper + +@Composable +fun SourceFilterScreen( + nestedScrollInterop: NestedScrollConnection, + presenter: SourceFilterPresenter, + onClickLang: (String) -> Unit, + onClickSource: (Source) -> Unit +) { + val state by presenter.state.collectAsState() + + when (state) { + is SourceFilterState.Loading -> LoadingScreen() + is SourceFilterState.Error -> Text(text = (state as SourceFilterState.Error).error!!.message!!) + is SourceFilterState.Success -> + SourceFilterContent( + nestedScrollInterop = nestedScrollInterop, + items = (state as SourceFilterState.Success).models, + onClickLang = onClickLang, + onClickSource = onClickSource, + ) + } +} + +@Composable +fun SourceFilterContent( + nestedScrollInterop: NestedScrollConnection, + items: List, + onClickLang: (String) -> Unit, + onClickSource: (Source) -> Unit +) { + if (items.isEmpty()) { + EmptyScreen(textResource = R.string.source_filter_empty_screen) + return + } + LazyColumn( + modifier = Modifier.nestedScroll(nestedScrollInterop) + ) { + items( + items = items, + contentType = { + when (it) { + is FilterUiModel.Header -> "header" + is FilterUiModel.Item -> "item" + } + }, + key = { + when (it) { + is FilterUiModel.Header -> it.hashCode() + is FilterUiModel.Item -> it.source.key() + } + } + ) { model -> + when (model) { + is FilterUiModel.Header -> { + SourceFilterHeader( + modifier = Modifier.animateItemPlacement(), + language = model.language, + isEnabled = model.isEnabled, + onClickItem = onClickLang + ) + } + is FilterUiModel.Item -> SourceFilterItem( + modifier = Modifier.animateItemPlacement(), + source = model.source, + isEnabled = model.isEnabled, + onClickItem = onClickSource + ) + } + } + } +} + +@Composable +fun SourceFilterHeader( + modifier: Modifier, + language: String, + isEnabled: Boolean, + onClickItem: (String) -> Unit +) { + PreferenceRow( + modifier = modifier, + title = LocaleHelper.getSourceDisplayName(language, LocalContext.current), + action = { + Switch(checked = isEnabled, onCheckedChange = null) + }, + onClick = { onClickItem(language) }, + ) +} + +@Composable +fun SourceFilterItem( + modifier: Modifier, + source: Source, + isEnabled: Boolean, + onClickItem: (Source) -> Unit +) { + BaseSourceItem( + modifier = modifier, + source = source, + showLanguageInContent = false, + onClickItem = { onClickItem(source) }, + action = { + Checkbox(checked = isEnabled, onCheckedChange = null) + } + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 551aa920ff..59a583bef8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import rx.Observable @@ -18,6 +19,8 @@ open class SourceManager(private val context: Context) { private val _catalogueSources: MutableStateFlow> = MutableStateFlow(listOf()) val catalogueSources: Flow> = _catalogueSources + val onlineSources: Flow> = + _catalogueSources.map { sources -> sources.filterIsInstance() } init { createInternalSources().forEach { registerSource(it) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt index 824f0670a2..ed86c9d118 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt @@ -48,7 +48,7 @@ class SourceController : SearchableComposeController() { openSource(source, BrowseSourceController(source)) }, onClickDisable = { source -> - presenter.disableSource(source) + presenter.toggleSource(source) }, onClickLatest = { source -> openSource(source, LatestUpdatesController(source)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt index bb1aaa7d98..b68cd3aa85 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt @@ -1,112 +1,34 @@ package eu.kanade.tachiyomi.ui.browse.source -import android.graphics.drawable.Drawable -import androidx.preference.CheckBoxPreference -import androidx.preference.PreferenceGroup -import androidx.preference.PreferenceScreen +import androidx.compose.runtime.Composable +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import eu.kanade.domain.source.model.Source +import eu.kanade.presentation.source.SourceFilterScreen import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.getPreferenceKey -import eu.kanade.tachiyomi.source.icon -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.setting.SettingsController -import eu.kanade.tachiyomi.util.preference.minusAssign -import eu.kanade.tachiyomi.util.preference.onChange -import eu.kanade.tachiyomi.util.preference.plusAssign -import eu.kanade.tachiyomi.util.preference.switchPreferenceCategory -import eu.kanade.tachiyomi.util.preference.titleRes -import eu.kanade.tachiyomi.util.system.LocaleHelper -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.TreeMap +import eu.kanade.tachiyomi.ui.base.controller.ComposeController -class SourceFilterController : SettingsController() { +class SourceFilterController : ComposeController() { - private val onlineSources by lazy { Injekt.get().getOnlineSources() } + override fun getTitle() = resources?.getString(R.string.label_sources) - override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { - titleRes = R.string.label_sources + override fun createPresenter(): SourceFilterPresenter = SourceFilterPresenter() - // Get the list of active language codes. - val activeLangsCodes = preferences.enabledLanguages().get() - - // Get a map of sources grouped by language. - val sourcesByLang = onlineSources.groupByTo(TreeMap(), { it.lang }) - - // Order first by active languages, then inactive ones - val orderedLangs = sourcesByLang.keys.sortedWith( - compareBy( - { it !in activeLangsCodes }, - { LocaleHelper.getSourceDisplayName(it, context) }, - ), + @Composable + override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { + SourceFilterScreen( + nestedScrollInterop = nestedScrollInterop, + presenter = presenter, + onClickLang = { language -> + presenter.toggleLanguage(language) + }, + onClickSource = { source -> + presenter.toggleSource(source) + }, ) - - orderedLangs.forEach { lang -> - val sources = sourcesByLang[lang].orEmpty().sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name })) - - // Create a preference group and set initial state and change listener - switchPreferenceCategory { - this@apply.addPreference(this) - title = LocaleHelper.getSourceDisplayName(lang, context) - isPersistent = false - if (lang in activeLangsCodes) { - setChecked(true) - addLanguageSources(this, sources) - } - - onChange { newValue -> - val checked = newValue as Boolean - if (!checked) { - preferences.enabledLanguages() -= lang - removeAll() - } else { - preferences.enabledLanguages() += lang - addLanguageSources(this, sources) - } - true - } - } - } - } - - override fun setDivider(divider: Drawable?) { - super.setDivider(null) - } - - /** - * Adds the source list for the given group (language). - * - * @param group the language category. - */ - private fun addLanguageSources(group: PreferenceGroup, sources: List) { - val disabledSourceIds = preferences.disabledSources().get() - - sources - .sortedBy { it.id.toString() in disabledSourceIds } - .map { source -> - CheckBoxPreference(group.context).apply { - val id = source.id.toString() - title = source.name - key = source.getPreferenceKey() - isPersistent = false - isChecked = id !in disabledSourceIds - - val sourceIcon = source.icon() - if (sourceIcon != null) { - icon = sourceIcon - } - - onChange { newValue -> - val checked = newValue as Boolean - if (checked) { - preferences.disabledSources() -= id - } else { - preferences.disabledSources() += id - } - true - } - } - } - .forEach { group.addPreference(it) } } } + +sealed class FilterUiModel { + data class Header(val language: String, val isEnabled: Boolean) : FilterUiModel() + data class Item(val source: Source, val isEnabled: Boolean) : FilterUiModel() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt new file mode 100644 index 0000000000..dd94b3d9d8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt @@ -0,0 +1,71 @@ +package eu.kanade.tachiyomi.ui.browse.source + +import android.os.Bundle +import eu.kanade.domain.source.interactor.GetLanguagesWithSources +import eu.kanade.domain.source.interactor.ToggleLanguage +import eu.kanade.domain.source.interactor.ToggleSource +import eu.kanade.domain.source.model.Source +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.lang.launchIO +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SourceFilterPresenter( + private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(), + private val toggleSource: ToggleSource = Injekt.get(), + private val toggleLanguage: ToggleLanguage = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get() +) : BasePresenter() { + + private val _state: MutableStateFlow = MutableStateFlow(SourceFilterState.Loading) + val state: StateFlow = _state.asStateFlow() + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + presenterScope.launchIO { + getLanguagesWithSources.subscribe() + .catch { exception -> + _state.emit(SourceFilterState.Error(exception)) + } + .collectLatest { sourceLangMap -> + val uiModels = sourceLangMap.toFilterUiModels() + _state.emit(SourceFilterState.Success(uiModels)) + } + } + } + + private fun Map>.toFilterUiModels(): List { + return this.flatMap { + val isLangEnabled = it.key in preferences.enabledLanguages().get() + val header = listOf(FilterUiModel.Header(it.key, isLangEnabled)) + + if (isLangEnabled.not()) return@flatMap header + header + it.value.map { source -> + FilterUiModel.Item( + source, + source.id.toString() !in preferences.disabledSources().get() + ) + } + } + } + + fun toggleSource(source: Source) { + toggleSource.await(source) + } + + fun toggleLanguage(language: String) { + toggleLanguage.await(language) + } +} + +sealed class SourceFilterState { + object Loading : SourceFilterState() + data class Error(val error: Throwable) : SourceFilterState() + data class Success(val models: List) : SourceFilterState() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt index d770abb8a7..e35ad10541 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt @@ -1,8 +1,8 @@ package eu.kanade.tachiyomi.ui.browse.source import android.os.Bundle -import eu.kanade.domain.source.interactor.DisableSource import eu.kanade.domain.source.interactor.GetEnabledSources +import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.model.Pin import eu.kanade.domain.source.model.Source @@ -24,7 +24,7 @@ import java.util.TreeMap */ class SourcePresenter( private val getEnabledSources: GetEnabledSources = Injekt.get(), - private val disableSource: DisableSource = Injekt.get(), + private val toggleSource: ToggleSource = Injekt.get(), private val toggleSourcePin: ToggleSourcePin = Injekt.get() ) : BasePresenter() { @@ -79,8 +79,8 @@ class SourcePresenter( } } - fun disableSource(source: Source) { - disableSource.await(source) + fun toggleSource(source: Source) { + toggleSource.await(source) } fun togglePin(source: Source) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae07985473..f159a82ffd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -711,6 +711,9 @@ History deleted Are you sure? All history will be lost. + + No installed source found + Source migration guide Select data to include