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 index 94d588c8d..e1510ca14 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt @@ -11,16 +11,19 @@ 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.list.OnListItemClickListener 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.explore.ui.adapter.ExploreListEventListener +import org.koitharu.kotatsu.explore.ui.model.ExploreItem +import org.koitharu.kotatsu.history.ui.HistoryActivity +import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.settings.SettingsActivity class ExploreFragment : BaseFragment(), RecyclerViewOwner, - SourcesHeaderEventListener { + ExploreListEventListener, OnListItemClickListener { private val viewModel by viewModel() private var exploreAdapter: ExploreAdapter? = null @@ -35,15 +38,15 @@ class ExploreFragment : BaseFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - exploreAdapter = ExploreAdapter(get(), viewLifecycleOwner, this) + exploreAdapter = ExploreAdapter(get(), viewLifecycleOwner, this, 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 + viewModel.content.observe(viewLifecycleOwner) { + exploreAdapter?.items = it } } @@ -68,6 +71,19 @@ class ExploreFragment : BaseFragment(), startActivity(SettingsActivity.newManageSourcesIntent(view.context)) } + override fun onClick(v: View) { + val intent = when (v.id) { + R.id.button_history -> HistoryActivity.newIntent(v.context) + else -> return + } + startActivity(intent) + } + + override fun onItemClick(item: ExploreItem.Source, view: View) { + val intent = MangaListActivity.newIntent(view.context, item.source) + startActivity(intent) + } + override fun onRetryClick(error: Throwable) = Unit override fun onEmptyActionClick() = Unit 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 index aa9d6f7af..c60b60eb0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt @@ -1,71 +1,40 @@ package org.koitharu.kotatsu.explore.ui -import androidx.core.os.LocaleListCompat -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.LiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart 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 = "!" +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct class ExploreViewModel( private val settings: AppSettings, ) : BaseViewModel() { - val items = MutableLiveData>(emptyList()) + val content: LiveData> = settings.observe() + .filter { it == AppSettings.KEY_SOURCES_HIDDEN || it == AppSettings.KEY_SOURCES_ORDER } + .onStart { emit("") } + .map { settings.getMangaSources(includeHidden = false) } + .distinctUntilChanged() + .map { buildList(it) } + .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, 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) + private fun buildList(sources: List): List { + val result = ArrayList(sources.size + 2) result += ExploreItem.Buttons - val enabledSources = map.remove(KEY_ENABLED) - if (!enabledSources.isNullOrEmpty()) { + if (sources.isNotEmpty()) { 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) - } + sources.mapTo(result) { ExploreItem.Source(it) } + } else { + // TODO } + return result } } \ 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 index 6e6fd597b..4d8c47455 100644 --- 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 @@ -3,25 +3,17 @@ package org.koitharu.kotatsu.explore.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.explore.ui.model.ExploreItem class ExploreAdapter( coil: ImageLoader, lifecycleOwner: LifecycleOwner, - listener: SourcesHeaderEventListener, + listener: ExploreListEventListener, + clickListener: OnListItemClickListener, ) : 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 + exploreButtonsAD(listener), + exploreSourcesHeaderAD(listener), + exploreSourceItemAD(coil, clickListener, 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 index a5cde1fb9..5df25dbc5 100644 --- 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 @@ -7,22 +7,29 @@ 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.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 +import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable -fun exploreButtonsDelegate() = adapterDelegateViewBinding( +fun exploreButtonsAD( + clickListener: View.OnClickListener, +) = adapterDelegateViewBinding( { layoutInflater, parent -> ItemExploreButtonsBinding.inflate(layoutInflater, parent, false) } ) { - binding.localStorage.requestFocus() // stub + binding.buttonBookmarks.setOnClickListener(clickListener) + binding.buttonHistory.setOnClickListener(clickListener) + binding.buttonLocal.setOnClickListener(clickListener) + binding.buttonSuggestions.setOnClickListener(clickListener) +} -} // TODO - -fun sourceHeaderDelegate( - listener: SourcesHeaderEventListener, +fun exploreSourcesHeaderAD( + listener: ExploreListEventListener, ) = adapterDelegateViewBinding( { layoutInflater, parent -> ItemExploreHeaderBinding.inflate(layoutInflater, parent, false) } ) { @@ -38,8 +45,9 @@ fun sourceHeaderDelegate( } } -fun sourceItemDelegate( +fun exploreSourceItemAD( coil: ImageLoader, + listener: OnListItemClickListener, lifecycleOwner: LifecycleOwner, ) = adapterDelegateViewBinding( { layoutInflater, parent -> ItemExploreSourceBinding.inflate(layoutInflater, parent, false) }, @@ -47,12 +55,19 @@ fun sourceItemDelegate( ) { var imageRequest: Disposable? = null + val eventListener = AdapterDelegateClickListenerAdapter(this, listener) + + binding.root.setOnClickListener(eventListener) + binding.root.setOnLongClickListener(eventListener) bind { binding.textViewTitle.text = item.source.title + val fallbackIcon = FaviconFallbackDrawable(context, item.source.name) imageRequest = ImageRequest.Builder(context) .data(item.faviconUrl) - .error(R.drawable.ic_favicon_fallback) + .fallback(fallbackIcon) + .placeholder(fallbackIcon) + .error(fallbackIcon) .target(binding.imageViewCover) .lifecycle(lifecycleOwner) .enqueueWith(coil) 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 index e017fd002..768db9b6b 100644 --- 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 @@ -8,9 +8,7 @@ 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.Buttons && newItem is ExploreItem.Buttons -> true oldItem is ExploreItem.Source && newItem is ExploreItem.Source -> { oldItem.source == newItem.source } 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/ExploreListEventListener.kt similarity index 67% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/SourcesHeaderEventListener.kt rename to app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreListEventListener.kt index bfd316192..4bd122086 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/SourcesHeaderEventListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreListEventListener.kt @@ -3,8 +3,7 @@ package org.koitharu.kotatsu.explore.ui.adapter import android.view.View import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener -interface SourcesHeaderEventListener : ListStateHolderListener { +interface ExploreListEventListener : ListStateHolderListener, View.OnClickListener { 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 index 8c67f699d..59e6d174a 100644 --- 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 @@ -2,9 +2,10 @@ package org.koitharu.kotatsu.explore.ui.model import android.net.Uri import androidx.annotation.StringRes +import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.MangaSource -sealed interface ExploreItem { +sealed interface ExploreItem : ListModel { object Buttons : ExploreItem @@ -24,7 +25,6 @@ sealed interface ExploreItem { class Source( val source: MangaSource, - val summary: String?, ) : ExploreItem { val faviconUrl: Uri @@ -37,15 +37,12 @@ sealed interface ExploreItem { 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 + return source.hashCode() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt index 4904ab34a..821a7d491 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags import org.koitharu.kotatsu.databinding.ActivityContainerBinding +import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel @@ -24,17 +25,21 @@ class MangaListActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityContainerBinding.inflate(layoutInflater)) - val tags = intent.getParcelableExtra(EXTRA_TAGS)?.tags ?: run { - finishAfterTransition() - return - } + val tags = intent.getParcelableExtra(EXTRA_TAGS)?.tags supportActionBar?.setDisplayHomeAsUpEnabled(true) val fm = supportFragmentManager if (fm.findFragmentById(R.id.container) == null) { + val source = intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: tags?.firstOrNull()?.source + if (source == null) { + finishAfterTransition() + return + } fm.commit { - val fragment = RemoteListFragment.newInstance(tags.first().source) + val fragment = RemoteListFragment.newInstance(source) replace(R.id.container, fragment) - runOnCommit(ApplyFilterRunnable(fragment, tags)) + if (!tags.isNullOrEmpty()) { + runOnCommit(ApplyFilterRunnable(fragment, tags)) + } } } } @@ -70,9 +75,12 @@ class MangaListActivity : BaseActivity() { companion object { private const val EXTRA_TAGS = "tags" + private const val EXTRA_SOURCE = "source" - fun newIntent(context: Context, tags: Set) = - Intent(context, MangaListActivity::class.java) - .putExtra(EXTRA_TAGS, ParcelableMangaTags(tags)) + fun newIntent(context: Context, tags: Set) = Intent(context, MangaListActivity::class.java) + .putExtra(EXTRA_TAGS, ParcelableMangaTags(tags)) + + fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java) + .putExtra(EXTRA_SOURCE, source) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt b/app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt new file mode 100644 index 000000000..6de655da8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt @@ -0,0 +1,66 @@ +package org.koitharu.kotatsu.utils.image + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.Drawable +import androidx.core.graphics.ColorUtils +import kotlin.math.absoluteValue + +class FaviconFallbackDrawable( + context: Context, + name: String, +) : Drawable() { + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val letter = name.take(1).uppercase() + private val color = colorOfString(name) + private val textBounds = Rect() + private val tempRect = Rect() + + init { + paint.style = Paint.Style.FILL + paint.textAlign = Paint.Align.CENTER + paint.isFakeBoldText = true + } + + override fun draw(canvas: Canvas) { + val cx = bounds.exactCenterX() + paint.color = color + canvas.drawPaint(paint) + paint.color = Color.WHITE + val ty = bounds.height() / 2f + textBounds.height() / 2f - textBounds.bottom + canvas.drawText(letter, cx, ty, paint) + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + val innerWidth = bounds.width() - (paint.strokeWidth * 2f) + paint.textSize = getTextSizeForWidth(innerWidth, "100%") + paint.getTextBounds(letter, 0, letter.length, textBounds) + invalidateSelf() + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + } + + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Deprecated in Java") + override fun getOpacity() = PixelFormat.TRANSPARENT + + private fun getTextSizeForWidth(width: Float, text: String): Float { + val testTextSize = 48f + paint.textSize = testTextSize + paint.getTextBounds(text, 0, text.length, tempRect) + return testTextSize * width / tempRect.width() + } + + private fun colorOfString(str: String): Int { + val hue = (str.hashCode() % 360).absoluteValue.toFloat() + return ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f)) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp/item_explore_buttons.xml b/app/src/main/res/layout-w600dp/item_explore_buttons.xml index 5e5d2e62a..38f396541 100644 --- a/app/src/main/res/layout-w600dp/item_explore_buttons.xml +++ b/app/src/main/res/layout-w600dp/item_explore_buttons.xml @@ -5,45 +5,45 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:paddingHorizontal="8dp" - android:paddingVertical="4dp"> + android:padding="4dp" + android:weightSum="4"> diff --git a/app/src/main/res/layout/item_explore_buttons.xml b/app/src/main/res/layout/item_explore_buttons.xml index af6ab2771..1cd5b385d 100644 --- a/app/src/main/res/layout/item_explore_buttons.xml +++ b/app/src/main/res/layout/item_explore_buttons.xml @@ -1,62 +1,63 @@ - + android:rowCount="2"> + app:icon="@drawable/ic_history" /> - - + app:icon="@drawable/ic_suggestion" /> + 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" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_explore_source.xml b/app/src/main/res/layout/item_explore_source.xml index 9f3fc3ba7..0dcd431ba 100644 --- a/app/src/main/res/layout/item_explore_source.xml +++ b/app/src/main/res/layout/item_explore_source.xml @@ -1,5 +1,5 @@ - + tools:text="@tools:sample/lorem[2]" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f665ed31e..a4b3ab1de 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -128,15 +128,17 @@