From 8c2bc078e586733a0af8bd737639f506f232bf4f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 17 Jun 2021 19:45:36 +0300 Subject: [PATCH] Manga languages onboarding --- .idea/misc.xml | 7 ++ app/build.gradle | 2 +- .../kotatsu/core/model/MangaSource.kt | 2 +- .../kotatsu/core/prefs/AppSettings.kt | 3 + .../koitharu/kotatsu/main/ui/MainActivity.kt | 3 + .../kotatsu/settings/SettingsModule.kt | 2 + .../settings/onboard/OnboardDialogFragment.kt | 86 +++++++++++++++++ .../settings/onboard/OnboardViewModel.kt | 95 +++++++++++++++++++ .../onboard/adapter/SourceLocaleAD.kt | 23 +++++ .../onboard/adapter/SourceLocalesAdapter.kt | 28 ++++++ .../settings/onboard/model/SourceLocale.kt | 20 ++++ .../sources/SourcesSettingsFragment.kt | 22 ++++- .../koitharu/kotatsu/utils/ext/LocaleExt.kt | 29 ++++++ app/src/main/res/drawable/ic_locale.xml | 11 +++ app/src/main/res/layout/dialog_onboard.xml | 29 ++++++ .../main/res/layout/item_source_locale.xml | 16 ++++ app/src/main/res/menu/opt_sources.xml | 12 +++ app/src/main/res/values-ru/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 19 files changed, 391 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt create mode 100644 app/src/main/res/drawable/ic_locale.xml create mode 100644 app/src/main/res/layout/dialog_onboard.xml create mode 100644 app/src/main/res/layout/item_source_locale.xml create mode 100644 app/src/main/res/menu/opt_sources.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 4fd703491..bb01f41af 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -14,6 +14,8 @@ + + @@ -23,6 +25,7 @@ + @@ -31,6 +34,7 @@ + @@ -41,9 +45,12 @@ + + + diff --git a/app/build.gradle b/app/build.gradle index 9e18f5bc5..2bc93b43f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,7 +74,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-process:2.3.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'androidx.recyclerview:recyclerview:1.2.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.work:work-runtime-ktx:2.5.0' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index 41476a63b..3f8f07973 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -37,7 +37,7 @@ enum class MangaSource( NINEMANGA_RU("NineManga Русский", "ru", NineMangaRepository.Russian::class.java), NINEMANGA_DE("NineManga Deutsch", "de", NineMangaRepository.Deutsch::class.java), NINEMANGA_IT("NineManga Italiano", "it", NineMangaRepository.Italiano::class.java), - NINEMANGA_BR("NineManga Brasil", "br", NineMangaRepository.Brazil::class.java), + NINEMANGA_BR("NineManga Brasil", "pt", NineMangaRepository.Brazil::class.java), NINEMANGA_FR("NineManga Français", "fr", NineMangaRepository.Francais::class.java), ; diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 7fc34cc4e..4456df455 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -101,6 +101,9 @@ class AppSettings private constructor(private val prefs: SharedPreferences) : var hiddenSources by StringSetPreferenceDelegate(KEY_SOURCES_HIDDEN) + val isSourcesSelected: Boolean + get() = KEY_SOURCES_HIDDEN in prefs + fun getStorageDir(context: Context): File? { val value = prefs.getString(KEY_LOCAL_STORAGE, null)?.let { File(it) diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index cd95e7e8a..bfb806231 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.commit import androidx.swiperefreshlayout.widget.CircularProgressDrawable import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar +import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity @@ -41,6 +42,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.search.ui.suggestion.SearchUI import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -86,6 +88,7 @@ class MainActivity : BaseActivity(), if (savedInstanceState == null) { TrackWorker.setup(applicationContext) AppUpdateChecker(this).launchIfNeeded() + OnboardDialogFragment.showWelcome(get(), supportFragmentManager) } viewModel.onOpenReader.observe(this, this::onOpenReader) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt index 222f20258..435847a8e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.backup.RestoreRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.settings.backup.BackupViewModel import org.koitharu.kotatsu.settings.backup.RestoreViewModel +import org.koitharu.kotatsu.settings.onboard.OnboardViewModel import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel val settingsModule @@ -21,4 +22,5 @@ val settingsModule viewModel { BackupViewModel(get(), androidContext()) } viewModel { (uri: Uri?) -> RestoreViewModel(uri, get(), androidContext()) } viewModel { ProtectSetupViewModel(get()) } + viewModel { OnboardViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt new file mode 100644 index 000000000..b6945a7e4 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt @@ -0,0 +1,86 @@ +package org.koitharu.kotatsu.settings.onboard + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentManager +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.databinding.DialogOnboardBinding +import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter +import org.koitharu.kotatsu.settings.onboard.model.SourceLocale +import org.koitharu.kotatsu.utils.ext.observeNotNull +import org.koitharu.kotatsu.utils.ext.withArgs + +class OnboardDialogFragment : AlertDialogFragment(), + OnListItemClickListener, DialogInterface.OnClickListener { + + private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) + private var isWelcome: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.run { + isWelcome = getBoolean(ARG_WELCOME, false) + } + } + + override fun onInflateView( + inflater: LayoutInflater, + container: ViewGroup?, + ) = DialogOnboardBinding.inflate(inflater, container, false) + + override fun onBuildDialog(builder: AlertDialog.Builder) { + builder + .setPositiveButton(R.string.done, this) + .setCancelable(true) + if (isWelcome) { + builder.setTitle(R.string.welcome) + } else { + builder + .setTitle(R.string.remote_sources) + .setNegativeButton(android.R.string.cancel, this) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val adapter = SourceLocalesAdapter(this) + binding.recyclerView.adapter = adapter + viewModel.list.observeNotNull(viewLifecycleOwner) { + adapter.items = it + } + } + + override fun onItemClick(item: SourceLocale, view: View) { + viewModel.setItemChecked(item.key, !item.isChecked) + } + + override fun onClick(dialog: DialogInterface?, which: Int) { + when (which) { + DialogInterface.BUTTON_POSITIVE -> viewModel.apply() + } + } + + companion object { + + private const val TAG = "OnboardDialog" + private const val ARG_WELCOME = "welcome" + + fun show(fm: FragmentManager) = OnboardDialogFragment().show(fm, TAG) + + fun showWelcome(settings: AppSettings, fm: FragmentManager) { + if (!settings.isSourcesSelected) { + OnboardDialogFragment().withArgs(1) { + putBoolean(ARG_WELCOME, true) + }.show(fm, TAG) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt new file mode 100644 index 000000000..9f8d9e65a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt @@ -0,0 +1,95 @@ +package org.koitharu.kotatsu.settings.onboard + +import androidx.collection.ArraySet +import androidx.core.os.LocaleListCompat +import androidx.lifecycle.MutableLiveData +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.settings.onboard.model.SourceLocale +import org.koitharu.kotatsu.utils.ext.map +import org.koitharu.kotatsu.utils.ext.mapTo +import org.koitharu.kotatsu.utils.ext.mapToSet +import java.util.* + +class OnboardViewModel( + private val settings: AppSettings, +) : BaseViewModel() { + + private val allSources = MangaSource.values().filterNot { x -> x == MangaSource.LOCAL } + + private val locales = allSources.mapTo(ArraySet()) { + it.locale + } + + private val selectedLocales = locales.toMutableSet() + + val list = MutableLiveData?>() + + init { + if (settings.isSourcesSelected) { + selectedLocales.removeAll(settings.hiddenSources.map { x -> MangaSource.valueOf(x).locale }) + } else { + LocaleListCompat.getDefault().mapTo(selectedLocales) { x -> + x.language + } + selectedLocales.retainAll(allSources.map { x -> x.locale }) + if (selectedLocales.isEmpty()) { + selectedLocales += "en" + } + } + rebuildList() + } + + fun setItemChecked(key: String?, isChecked: Boolean) { + val isModified = if (isChecked) { + selectedLocales.add(key) + } else { + selectedLocales.remove(key) + } + if (isModified) { + rebuildList() + } + } + + fun apply() { + settings.hiddenSources = allSources.filterNot { x -> + x.locale in selectedLocales + }.mapToSet { x -> x.name } + } + + private fun rebuildList() { + list.value = locales.map { key -> + val locale = if (key != null) { + Locale(key) + } else null + SourceLocale( + key = key, + title = locale?.getDisplayLanguage(locale)?.capitalize(locale), + isChecked = key in selectedLocales + ) + }.sortedWith(SourceLocaleComparator()) + } + + private class SourceLocaleComparator : Comparator { + + private val deviceLocales = LocaleListCompat.getAdjustedDefault() + .map { it.language } + + override fun compare(a: SourceLocale?, b: SourceLocale?): Int { + return when { + a === b -> 0 + a?.key == null -> 1 + b?.key == null -> -1 + else -> { + val index = deviceLocales.indexOf(a.key) + if (index == -1) { + compareValues(a.title, b.title) + } else { + -2 - index + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt new file mode 100644 index 000000000..d46bafd3d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.settings.onboard.adapter + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.databinding.ItemSourceLocaleBinding +import org.koitharu.kotatsu.settings.onboard.model.SourceLocale + +fun sourceLocaleAD( + clickListener: OnListItemClickListener +) = adapterDelegateViewBinding( + { inflater, parent -> ItemSourceLocaleBinding.inflate(inflater, parent, false) } +) { + + binding.root.setOnClickListener { + clickListener.onItemClick(item, it) + } + + bind { + binding.root.text = item.title ?: getString(R.string.other) + binding.root.isChecked = item.isChecked + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt new file mode 100644 index 000000000..0d4112f31 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.settings.onboard.adapter + +import androidx.recyclerview.widget.DiffUtil +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.settings.onboard.model.SourceLocale + +class SourceLocalesAdapter( + clickListener: OnListItemClickListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + delegatesManager.addDelegate(sourceLocaleAD(clickListener)) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: SourceLocale, + newItem: SourceLocale, + ): Boolean = oldItem.key == newItem.key + + override fun areContentsTheSame( + oldItem: SourceLocale, + newItem: SourceLocale, + ): Boolean = oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt new file mode 100644 index 000000000..c2bce3718 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt @@ -0,0 +1,20 @@ +package org.koitharu.kotatsu.settings.onboard.model + +import java.util.* + +data class SourceLocale( + val key: String?, + val title: String?, + val isChecked: Boolean, +) : Comparable { + + override fun compareTo(other: SourceLocale): Int { + return when { + this === other -> 0 + key == Locale.getDefault().language -> -2 + key == null -> 1 + other.key == null -> -1 + else -> compareValues(title, other.title) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index 9c30c32cb..ac2d96c79 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -1,9 +1,7 @@ package org.koitharu.kotatsu.settings.sources import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.core.graphics.Insets import androidx.core.view.updatePadding import androidx.recyclerview.widget.DividerItemDecoration @@ -16,6 +14,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment class SourcesSettingsFragment : BaseFragment(), OnListItemClickListener { @@ -25,6 +24,7 @@ class SourcesSettingsFragment : BaseFragment(), override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) reorderHelper = ItemTouchHelper(SourcesReorderCallback()) + setHasOptionsMenu(true) } override fun onInflateView( @@ -51,6 +51,22 @@ class SourcesSettingsFragment : BaseFragment(), super.onDestroyView() } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + // TODO handle changes in dialog + // inflater.inflate(R.menu.opt_sources, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when(item.itemId) { + R.id.action_languages -> { + OnboardDialogFragment.show(parentFragmentManager) + true + } + else -> super.onOptionsItemSelected(item) + } + } + override fun onWindowInsetsChanged(insets: Insets) { binding.recyclerView.updatePadding( bottom = insets.bottom, diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt new file mode 100644 index 000000000..1ad4cc003 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt @@ -0,0 +1,29 @@ +package org.koitharu.kotatsu.utils.ext + +import androidx.core.os.LocaleListCompat +import java.util.* +import kotlin.collections.ArrayList + +fun LocaleListCompat.toList(): List { + val list = ArrayList(size()) + for (i in 0 until size()) { + list += get(i) + } + return list +} + +inline fun > LocaleListCompat.mapTo( + destination: C, + block: (Locale) -> R, +): C { + val len = size() + for (i in 0 until len) { + val item = get(i) + destination.add(block(item)) + } + return destination +} + +inline fun LocaleListCompat.map(block: (Locale) -> T): List { + return mapTo(ArrayList(size()), block) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_locale.xml b/app/src/main/res/drawable/ic_locale.xml new file mode 100644 index 000000000..1bc2d39de --- /dev/null +++ b/app/src/main/res/drawable/ic_locale.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/dialog_onboard.xml b/app/src/main/res/layout/dialog_onboard.xml new file mode 100644 index 000000000..2845055a6 --- /dev/null +++ b/app/src/main/res/layout/dialog_onboard.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_source_locale.xml b/app/src/main/res/layout/item_source_locale.xml new file mode 100644 index 000000000..1ccef475e --- /dev/null +++ b/app/src/main/res/layout/item_source_locale.xml @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_sources.xml b/app/src/main/res/menu/opt_sources.xml new file mode 100644 index 000000000..35c7034be --- /dev/null +++ b/app/src/main/res/menu/opt_sources.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b20c30211..4638416bc 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -207,4 +207,7 @@ Пароль должен содержать не менее 4 символов Прятать заголовок при прокрутке Поиск только по %s + Другие + Languages + Welcome \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64fb9d0bc..8112a0029 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -210,4 +210,7 @@ Hide toolbar when scrolling Search only on %s Do you really want to remove all recent search queries? This action cannot be undone. + Other + Languages + Welcome \ No newline at end of file