diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 88ea8c25d..33b999667 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -21,6 +21,7 @@ import org.koitharu.kotatsu.core.network.networkModule import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.uiModule import org.koitharu.kotatsu.details.detailsModule +import org.koitharu.kotatsu.explore.exploreModule import org.koitharu.kotatsu.favourites.favouritesModule import org.koitharu.kotatsu.history.historyModule import org.koitharu.kotatsu.library.libraryModule @@ -79,6 +80,7 @@ class KotatsuApp : Application() { shikimoriModule, bookmarksModule, libraryModule, + exploreModule, ) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt index 9cdce9654..f97708e00 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt @@ -25,6 +25,7 @@ import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.settings.SettingsActivity abstract class BaseActivity : AppCompatActivity(), diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 711ccdeb9..912a09681 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -332,7 +332,8 @@ class DetailsFragment : override fun onWindowInsetsChanged(insets: Insets) { binding.root.updatePadding( - bottom = insets.bottom, + left = insets.left, + right = insets.right, ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ExploreModule.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ExploreModule.kt new file mode 100644 index 000000000..54a15a972 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ExploreModule.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.explore + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module +import org.koitharu.kotatsu.explore.ui.ExploreViewModel + +val exploreModule + get() = module { + viewModel { ExploreViewModel(get()) } + } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt new file mode 100644 index 000000000..94d588c8d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt @@ -0,0 +1,79 @@ +package org.koitharu.kotatsu.explore.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.graphics.Insets +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.RecyclerView +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.BaseFragment +import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.databinding.FragmentExploreBinding +import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter +import org.koitharu.kotatsu.explore.ui.adapter.SourcesHeaderEventListener +import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter +import org.koitharu.kotatsu.settings.SettingsActivity + +class ExploreFragment : BaseFragment(), + RecyclerViewOwner, + SourcesHeaderEventListener { + + private val viewModel by viewModel() + private var exploreAdapter: ExploreAdapter? = null + private var paddingHorizontal = 0 + + override val recyclerView: RecyclerView + get() = binding.recyclerView + + override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentExploreBinding { + return FragmentExploreBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + exploreAdapter = ExploreAdapter(get(), viewLifecycleOwner, this) + with(binding.recyclerView) { + adapter = exploreAdapter + setHasFixedSize(true) + val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) + paddingHorizontal = spacing + } + viewModel.items.observe(viewLifecycleOwner) { + exploreAdapter!!.items = it + } + } + + override fun onDestroyView() { + super.onDestroyView() + exploreAdapter = null + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.root.updatePadding( + left = insets.left, + right = insets.right, + ) + binding.recyclerView.updatePadding( + left = insets.left + paddingHorizontal, + right = insets.right + paddingHorizontal, + bottom = insets.bottom, + ) + } + + override fun onManageClick(view: View) { + startActivity(SettingsActivity.newManageSourcesIntent(view.context)) + } + + override fun onRetryClick(error: Throwable) = Unit + + override fun onEmptyActionClick() = Unit + + companion object { + + fun newInstance() = ExploreFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt new file mode 100644 index 000000000..aa9d6f7af --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt @@ -0,0 +1,71 @@ +package org.koitharu.kotatsu.explore.ui + +import androidx.core.os.LocaleListCompat +import androidx.lifecycle.MutableLiveData +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.getLocaleTitle +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.explore.ui.model.ExploreItem +import org.koitharu.kotatsu.utils.ext.map +import java.util.* + +private const val KEY_ENABLED = "!" + +class ExploreViewModel( + private val settings: AppSettings, +) : BaseViewModel() { + + val items = MutableLiveData>(emptyList()) + + init { + buildList() + } + + private fun buildList() { + val sources = settings.getMangaSources(includeHidden = true) + val hiddenSources = settings.hiddenSources + val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) { + if (it.name !in hiddenSources) { + KEY_ENABLED + } else { + it.locale + } + } + val result = ArrayList(sources.size + map.size + 1) + result += ExploreItem.Buttons + val enabledSources = map.remove(KEY_ENABLED) + if (!enabledSources.isNullOrEmpty()) { + result += ExploreItem.Header(R.string.enabled_sources) + enabledSources.mapTo(result) { + ExploreItem.Source( + source = it, + summary = it.getLocaleTitle(), + ) + } + } + items.value = result + } + + private class LocaleKeyComparator : Comparator { + + private val deviceLocales = LocaleListCompat.getAdjustedDefault() + .map { it.language } + + override fun compare(a: String?, b: String?): Int { + when { + a == b -> return 0 + a == null -> return 1 + b == null -> return -1 + } + val ai = deviceLocales.indexOf(a!!) + val bi = deviceLocales.indexOf(b!!) + return when { + ai < 0 && bi < 0 -> a.compareTo(b) + ai < 0 -> 1 + bi < 0 -> -1 + else -> ai.compareTo(bi) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt new file mode 100644 index 000000000..6e6fd597b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt @@ -0,0 +1,27 @@ +package org.koitharu.kotatsu.explore.ui.adapter + +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.explore.ui.model.ExploreItem + +class ExploreAdapter( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + listener: SourcesHeaderEventListener, +) : AsyncListDifferDelegationAdapter( + ExploreDiffCallback(), + exploreButtonsDelegate(), + sourceHeaderDelegate(listener), + sourceItemDelegate(coil, lifecycleOwner), +) { + + init { + delegatesManager + .addDelegate(exploreButtonsDelegate()) + .addDelegate(sourceHeaderDelegate(listener = listener)) + .addDelegate(sourceItemDelegate(coil, lifecycleOwner)) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt new file mode 100644 index 000000000..a5cde1fb9 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt @@ -0,0 +1,65 @@ +package org.koitharu.kotatsu.explore.ui.adapter + +import android.view.View +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader +import coil.request.Disposable +import coil.request.ImageRequest +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding +import org.koitharu.kotatsu.databinding.ItemExploreHeaderBinding +import org.koitharu.kotatsu.databinding.ItemExploreSourceBinding +import org.koitharu.kotatsu.explore.ui.model.ExploreItem +import org.koitharu.kotatsu.utils.ext.enqueueWith + +fun exploreButtonsDelegate() = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemExploreButtonsBinding.inflate(layoutInflater, parent, false) } +) { + + binding.localStorage.requestFocus() // stub + +} // TODO + +fun sourceHeaderDelegate( + listener: SourcesHeaderEventListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemExploreHeaderBinding.inflate(layoutInflater, parent, false) } +) { + + val listenerAdapter = View.OnClickListener { + listener.onManageClick(itemView) + } + + binding.buttonMore.setOnClickListener(listenerAdapter) + + bind { + binding.textViewTitle.setText(R.string.remote_sources) + } +} + +fun sourceItemDelegate( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemExploreSourceBinding.inflate(layoutInflater, parent, false) }, + on = { item, _, _ -> item is ExploreItem.Source } +) { + + var imageRequest: Disposable? = null + + bind { + binding.textViewTitle.text = item.source.title + imageRequest = ImageRequest.Builder(context) + .data(item.faviconUrl) + .error(R.drawable.ic_favicon_fallback) + .target(binding.imageViewCover) + .lifecycle(lifecycleOwner) + .enqueueWith(coil) + } + + onViewRecycled { + imageRequest?.dispose() + imageRequest = null + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreDiffCallback.kt new file mode 100644 index 000000000..e017fd002 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreDiffCallback.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.explore.ui.adapter + +import androidx.recyclerview.widget.DiffUtil +import org.koitharu.kotatsu.explore.ui.model.ExploreItem + +class ExploreDiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean { + return when { + oldItem.javaClass != newItem.javaClass -> false + oldItem is ExploreItem.Buttons && newItem is ExploreItem.Buttons -> { + oldItem == newItem + } + oldItem is ExploreItem.Source && newItem is ExploreItem.Source -> { + oldItem.source == newItem.source + } + oldItem is ExploreItem.Header && newItem is ExploreItem.Header -> { + oldItem.titleResId == newItem.titleResId + } + else -> false + } + } + + override fun areContentsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean { + return oldItem == newItem + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/SourcesHeaderEventListener.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/SourcesHeaderEventListener.kt new file mode 100644 index 000000000..bfd316192 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/SourcesHeaderEventListener.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.explore.ui.adapter + +import android.view.View +import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener + +interface SourcesHeaderEventListener : ListStateHolderListener { + + fun onManageClick(view: View) + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt new file mode 100644 index 000000000..8c67f699d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt @@ -0,0 +1,52 @@ +package org.koitharu.kotatsu.explore.ui.model + +import android.net.Uri +import androidx.annotation.StringRes +import org.koitharu.kotatsu.parsers.model.MangaSource + +sealed interface ExploreItem { + + object Buttons : ExploreItem + + class Header( + @StringRes val titleResId: Int, + ) : ExploreItem { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Header + return titleResId == other.titleResId + } + + override fun hashCode(): Int = titleResId + } + + class Source( + val source: MangaSource, + val summary: String?, + ) : ExploreItem { + + val faviconUrl: Uri + get() = Uri.fromParts("favicon", source.name, null) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Source + + if (source != other.source) return false + if (summary != other.summary) return false + + return true + } + + override fun hashCode(): Int { + var result = source.hashCode() + result = 31 * result + summary.hashCode() + return result + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryActivity.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryActivity.kt index 198aac283..b81bae96e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryActivity.kt @@ -3,7 +3,9 @@ package org.koitharu.kotatsu.history.ui import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.ViewGroup import androidx.core.graphics.Insets +import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.commit import org.koitharu.kotatsu.R @@ -26,7 +28,11 @@ class HistoryActivity : BaseActivity() { } override fun onWindowInsetsChanged(insets: Insets) { - binding.toolbar.updatePadding( + binding.toolbar.updateLayoutParams { + leftMargin = insets.left + rightMargin = insets.right + } + binding.root.updatePadding( left = insets.left, right = insets.right, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt index 16025b130..fc3322e4c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt @@ -99,9 +99,11 @@ class LibraryFragment : BaseFragment(), LibraryListEvent override fun onEmptyActionClick() = Unit override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + binding.root.updatePadding( left = insets.left, right = insets.right, + ) + binding.recyclerView.updatePadding( bottom = insets.bottom, ) } 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 f735f05f4..4b4b26dab 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 @@ -29,6 +29,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.databinding.ActivityMainBinding import org.koitharu.kotatsu.details.ui.DetailsActivity +import org.koitharu.kotatsu.explore.ui.ExploreFragment import org.koitharu.kotatsu.favourites.ui.FavouritesContainerFragment import org.koitharu.kotatsu.library.ui.LibraryFragment import org.koitharu.kotatsu.parsers.model.Manga @@ -248,8 +249,8 @@ class MainActivity : binding.root.isLiftAppBarOnScroll = true // придумать лучше } R.id.nav_explore -> { - setPrimaryFragment(FavouritesContainerFragment.newInstance()) - binding.root.isLiftAppBarOnScroll = false // --//-- + setPrimaryFragment(ExploreFragment.newInstance()) + binding.root.isLiftAppBarOnScroll = true // --//-- } R.id.nav_feed -> { setPrimaryFragment(FeedFragment.newInstance()) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt index 91f9c5987..91c8a34f3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -24,6 +24,7 @@ import org.koitharu.kotatsu.databinding.ActivitySettingsBinding import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment +import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment import org.koitharu.kotatsu.utils.ext.isScrolledToTop class SettingsActivity : @@ -126,6 +127,7 @@ class SettingsActivity : ACTION_SOURCE -> SourceSettingsFragment.newInstance( intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL ) + ACTION_MANAGE_SOURCES -> SourcesSettingsFragment() else -> SettingsHeadersFragment() } supportFragmentManager.commit { @@ -150,6 +152,7 @@ class SettingsActivity : private const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER" private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS" private const val ACTION_SHIKIMORI = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SHIKIMORI_SETTINGS" + private const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST" private const val EXTRA_SOURCE = "source" private const val HOST_SHIKIMORI_AUTH = "shikimori-auth" @@ -172,6 +175,10 @@ class SettingsActivity : Intent(context, SettingsActivity::class.java) .setAction(ACTION_TRACKER) + fun newManageSourcesIntent(context: Context) = + Intent(context, SettingsActivity::class.java) + .setAction(ACTION_MANAGE_SOURCES) + fun newSourceSettingsIntent(context: Context, source: MangaSource) = Intent(context, SettingsActivity::class.java) .setAction(ACTION_SOURCE) diff --git a/app/src/main/res/layout-w600dp/item_explore_buttons.xml b/app/src/main/res/layout-w600dp/item_explore_buttons.xml new file mode 100644 index 000000000..5e5d2e62a --- /dev/null +++ b/app/src/main/res/layout-w600dp/item_explore_buttons.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_explore.xml b/app/src/main/res/layout/fragment_explore.xml index 44cb66b71..6d0be0260 100644 --- a/app/src/main/res/layout/fragment_explore.xml +++ b/app/src/main/res/layout/fragment_explore.xml @@ -1,7 +1,17 @@ - - - \ No newline at end of file + android:layout_height="match_parent" + android:clipToPadding="false" + android:orientation="vertical" + android:paddingLeft="@dimen/list_spacing" + android:paddingRight="@dimen/list_spacing" + android:paddingTop="@dimen/grid_spacing_outer" + android:paddingBottom="@dimen/grid_spacing_outer" + android:scrollbars="vertical" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_explore_source" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_explore_buttons.xml b/app/src/main/res/layout/item_explore_buttons.xml new file mode 100644 index 000000000..af6ab2771 --- /dev/null +++ b/app/src/main/res/layout/item_explore_buttons.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_explore_card.xml b/app/src/main/res/layout/item_explore_card.xml deleted file mode 100644 index 272ebe645..000000000 --- a/app/src/main/res/layout/item_explore_card.xml +++ /dev/null @@ -1,9 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_explore_header.xml b/app/src/main/res/layout/item_explore_header.xml new file mode 100644 index 000000000..509869242 --- /dev/null +++ b/app/src/main/res/layout/item_explore_header.xml @@ -0,0 +1,27 @@ + + + + + +