Improve explore screen

This commit is contained in:
Koitharu
2022-07-07 17:50:05 +03:00
parent 6df56c2d77
commit 242704f853
17 changed files with 266 additions and 65 deletions

View File

@@ -66,6 +66,9 @@
<activity
android:name="org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity"
android:label="@string/bookmarks" />
<activity
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
android:label="@string/suggestions" />
<activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:exported="true"

View File

@@ -22,10 +22,12 @@ import org.koitharu.kotatsu.history.ui.HistoryActivity
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
class ExploreFragment : BaseFragment<FragmentExploreBinding>(),
RecyclerViewOwner,
ExploreListEventListener, OnListItemClickListener<ExploreItem.Source> {
ExploreListEventListener,
OnListItemClickListener<ExploreItem.Source> {
private val viewModel by viewModel<ExploreViewModel>()
private var exploreAdapter: ExploreAdapter? = null
@@ -78,6 +80,7 @@ class ExploreFragment : BaseFragment<FragmentExploreBinding>(),
R.id.button_history -> HistoryActivity.newIntent(v.context)
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)
R.id.button_bookmarks -> BookmarksActivity.newIntent(v.context)
R.id.button_suggestions -> SuggestionsActivity.newIntent(v.context)
else -> return
}
startActivity(intent)
@@ -90,7 +93,7 @@ class ExploreFragment : BaseFragment<FragmentExploreBinding>(),
override fun onRetryClick(error: Throwable) = Unit
override fun onEmptyActionClick() = Unit
override fun onEmptyActionClick() = onManageClick(requireView())
companion object {

View File

@@ -19,7 +19,11 @@ class ExploreViewModel(
) : BaseViewModel() {
val content: LiveData<List<ExploreItem>> = settings.observe()
.filter { it == AppSettings.KEY_SOURCES_HIDDEN || it == AppSettings.KEY_SOURCES_ORDER }
.filter {
it == AppSettings.KEY_SOURCES_HIDDEN ||
it == AppSettings.KEY_SOURCES_ORDER ||
it == AppSettings.KEY_SUGGESTIONS
}
.onStart { emit("") }
.map { settings.getMangaSources(includeHidden = false) }
.distinctUntilChanged()
@@ -27,13 +31,20 @@ class ExploreViewModel(
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
private fun buildList(sources: List<MangaSource>): List<ExploreItem> {
val result = ArrayList<ExploreItem>(sources.size + 2)
result += ExploreItem.Buttons
val result = ArrayList<ExploreItem>(sources.size + 3)
result += ExploreItem.Buttons(
isSuggestionsEnabled = settings.isSuggestionsEnabled,
)
result += ExploreItem.Header(R.string.remote_sources, sources.isNotEmpty())
if (sources.isNotEmpty()) {
result += ExploreItem.Header(R.string.enabled_sources)
sources.mapTo(result) { ExploreItem.Source(it) }
} else {
// TODO
result += ExploreItem.EmptyHint(
icon = R.drawable.ic_empty_search,
textPrimary = R.string.no_manga_sources,
textSecondary = R.string.no_manga_sources_text,
actionStringRes = R.string.manage,
)
}
return result
}

View File

@@ -16,4 +16,5 @@ class ExploreAdapter(
exploreButtonsAD(listener),
exploreSourcesHeaderAD(listener),
exploreSourceItemAD(coil, clickListener, lifecycleOwner),
exploreEmptyHintListAD(listener),
)

View File

@@ -1,19 +1,22 @@
package org.koitharu.kotatsu.explore.ui.adapter
import android.view.View
import androidx.core.view.isVisible
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.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
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.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable
fun exploreButtonsAD(
@@ -26,6 +29,10 @@ fun exploreButtonsAD(
binding.buttonHistory.setOnClickListener(clickListener)
binding.buttonLocal.setOnClickListener(clickListener)
binding.buttonSuggestions.setOnClickListener(clickListener)
bind {
binding.buttonSuggestions.isVisible = item.isSuggestionsEnabled
}
}
fun exploreSourcesHeaderAD(
@@ -41,7 +48,8 @@ fun exploreSourcesHeaderAD(
binding.buttonMore.setOnClickListener(listenerAdapter)
bind {
binding.textViewTitle.setText(R.string.remote_sources)
binding.textViewTitle.setText(item.titleResId)
binding.buttonMore.isVisible = item.isButtonVisible
}
}
@@ -77,4 +85,20 @@ fun exploreSourceItemAD(
imageRequest?.dispose()
imageRequest = null
}
}
fun exploreEmptyHintListAD(
listener: ListStateHolderListener,
) = adapterDelegateViewBinding<ExploreItem.EmptyHint, ExploreItem, ItemEmptyCardBinding>(
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) }
) {
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
bind {
binding.icon.setImageResource(item.icon)
binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
}
}

View File

@@ -1,26 +1,56 @@
package org.koitharu.kotatsu.explore.ui.model
import android.net.Uri
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface ExploreItem : ListModel {
object Buttons : ExploreItem
class Header(
@StringRes val titleResId: Int,
class Buttons(
val isSuggestionsEnabled: Boolean
) : 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
other as Buttons
if (isSuggestionsEnabled != other.isSuggestionsEnabled) return false
return true
}
override fun hashCode(): Int = titleResId
override fun hashCode(): Int {
return isSuggestionsEnabled.hashCode()
}
}
class Header(
@StringRes val titleResId: Int,
val isButtonVisible: Boolean,
) : ExploreItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Header
if (titleResId != other.titleResId) return false
if (isButtonVisible != other.isButtonVisible) return false
return true
}
override fun hashCode(): Int {
var result = titleResId
result = 31 * result + isButtonVisible.hashCode()
return result
}
}
class Source(
@@ -46,4 +76,10 @@ sealed interface ExploreItem : ListModel {
}
}
class EmptyHint(
@DrawableRes icon: Int,
@StringRes textPrimary: Int,
@StringRes textSecondary: Int,
@StringRes actionStringRes: Int,
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
}

View File

@@ -38,7 +38,7 @@ class LibraryMenuProvider(
}
private fun showClearHistoryDialog() {
val selectionListener = RememberSelectionDialogListener(-1)
val selectionListener = RememberSelectionDialogListener(2)
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
.setTitle(R.string.clear_history)
.setSingleChoiceItems(

View File

@@ -168,21 +168,20 @@ abstract class MangaListFragment :
}
override fun onWindowInsetsChanged(insets: Insets) {
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
binding.recyclerView.updatePadding(
bottom = insets.bottom,
)
if (activity is MainActivity) {
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
binding.swipeRefreshLayout.setProgressViewOffset(
true,
headerHeight + resources.resolveDp(-72),
headerHeight + resources.resolveDp(10),
)
} else {
binding.recyclerView.updatePadding(
bottom = insets.bottom,
)
}
}

View File

@@ -3,9 +3,32 @@ package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
data class EmptyState(
open class EmptyState(
@DrawableRes val icon: Int,
@StringRes val textPrimary: Int,
@StringRes val textSecondary: Int,
@StringRes val actionStringRes: Int,
) : ListModel
) : ListModel {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EmptyState
if (icon != other.icon) return false
if (textPrimary != other.textPrimary) return false
if (textSecondary != other.textSecondary) return false
if (actionStringRes != other.actionStringRes) return false
return true
}
override fun hashCode(): Int {
var result = icon
result = 31 * result + textPrimary
result = 31 * result + textSecondary
result = 31 * result + actionStringRes
return result
}
}

View File

@@ -30,7 +30,6 @@ 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
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -42,6 +41,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.settings.AppUpdateChecker
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
@@ -256,6 +256,10 @@ class MainActivity :
setPrimaryFragment(FeedFragment.newInstance())
binding.root.isLiftAppBarOnScroll = true // --//--
}
R.id.nav_tools -> {
setPrimaryFragment(SettingsHeadersFragment())
binding.root.isLiftAppBarOnScroll = true // --//--
}
else -> return false
}
appBar.setExpanded(true)

View File

@@ -55,13 +55,7 @@ class MangaListActivity : BaseActivity<ActivityContainerBinding>() {
left = insets.left,
right = insets.right
)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
binding.container.updatePadding(
bottom = insets.bottom
)
}
private class ApplyFilterRunnable(

View File

@@ -5,7 +5,6 @@ import android.os.Bundle
import android.view.View
import androidx.preference.ListPreference
import androidx.preference.Preference
import java.io.File
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
@@ -19,6 +18,7 @@ import org.koitharu.kotatsu.settings.utils.SliderPreference
import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import java.io.File
class ContentSettingsFragment :
BasePreferenceFragment(R.string.content),
@@ -30,9 +30,6 @@ class ContentSettingsFragment :
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_content)
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
)
findPreference<SliderPreference>(AppSettings.KEY_DOWNLOADS_PARALLELISM)?.run {
summary = value.toString()
setOnPreferenceChangeListener { preference, newValue ->
@@ -54,6 +51,9 @@ class ContentSettingsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
)
bindRemoteSourcesSummary()
settings.subscribe(this)
}

View File

@@ -0,0 +1,45 @@
package org.koitharu.kotatsu.suggestions.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
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
class SuggestionsActivity : BaseActivity<ActivityContainerBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
val fragment = SuggestionsFragment.newInstance()
replace(R.id.container, fragment)
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = insets.left
rightMargin = insets.right
}
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
}
companion object {
fun newIntent(context: Context) = Intent(context, SuggestionsActivity::class.java)
}
}

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentPadding="@dimen/margin_normal">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/icon"
android:layout_width="120dp"
android:layout_height="120dp"
android:contentDescription="@null"
android:scaleType="fitCenter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_empty_favourites" />
<TextView
android:id="@+id/textPrimary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:textAppearance="?attr/textAppearanceTitleLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem[3]" />
<TextView
android:id="@+id/textSecondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/margin_small"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toBottomOf="@id/textPrimary"
tools:text="@tools:sample/lorem[15]" />
<Button
android:id="@+id/button_retry"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/textSecondary"
tools:text="@string/try_again"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -1,63 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<GridLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:orientation="vertical"
android:rowCount="2">
android:paddingHorizontal="8dp"
android:paddingVertical="4dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_history"
style="@style/Widget.Kotatsu.ExploreButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="0.5"
android:layout_gravity="fill_horizontal"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:text="@string/history"
app:icon="@drawable/ic_history" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_suggestions"
style="@style/Widget.Kotatsu.ExploreButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="0.5"
android:layout_gravity="fill_horizontal"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:text="@string/suggestions"
app:icon="@drawable/ic_suggestion" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_local"
style="@style/Widget.Kotatsu.ExploreButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="0.5"
android:layout_gravity="fill_horizontal"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:text="@string/local_storage"
app:icon="@drawable/ic_storage" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_bookmarks"
style="@style/Widget.Kotatsu.ExploreButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="0.5"
android:layout_gravity="fill_horizontal"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:text="@string/bookmarks"
app:icon="@drawable/ic_bookmark" />
</GridLayout>
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flow"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:constraint_referenced_ids="button_history,button_local,button_suggestions,button_bookmarks"
app:flow_horizontalGap="8dp"
app:flow_maxElementsWrap="2"
app:flow_verticalGap="4dp"
app:flow_wrapMode="aligned"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -327,4 +327,6 @@
<string name="no_bookmarks_yet">No bookmarks yet</string>
<string name="no_bookmarks_summary">You can create bookmark while reading manga</string>
<string name="bookmarks_removed">Bookmarks removed</string>
<string name="no_manga_sources">No manga sources</string>
<string name="no_manga_sources_text">Enable manga sources to read manga online</string>
</resources>

View File

@@ -130,7 +130,7 @@
<style name="Widget.Kotatsu.ExploreButton" parent="Widget.Material3.Button.TonalButton.Icon">
<item name="android:minHeight">58dp</item>
<item name="android:textColor">?attr/colorOnSurface</item>
<item name="singleLine">true</item>
<item name="android:singleLine">true</item>
<item name="shapeAppearance">?shapeAppearanceCornerLarge</item>
<item name="iconPadding">16dp</item>
<item name="iconGravity">start</item>