Show sources pinned icons

master
Koitharu 2 years ago
parent eba1679761
commit 6d07c335de
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 651 versionCode = 652
versionName = '7.3' versionName = '7.4-a1'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {

@ -38,7 +38,8 @@ fun MangaSource(name: String?): MangaSource {
return UnknownMangaSource return UnknownMangaSource
} }
fun MangaSource.isNsfw() = when (this) { fun MangaSource.isNsfw(): Boolean = when (this) {
is MangaSourceInfo -> mangaSource.isNsfw()
is MangaParserSource -> contentType == ContentType.HENTAI is MangaParserSource -> contentType == ContentType.HENTAI
else -> false else -> false
} }
@ -53,6 +54,7 @@ val ContentType.titleResId
} }
fun MangaSource.getSummary(context: Context): String? = when (this) { fun MangaSource.getSummary(context: Context): String? = when (this) {
is MangaSourceInfo -> mangaSource.getSummary(context)
is MangaParserSource -> { is MangaParserSource -> {
val type = context.getString(contentType.titleResId) val type = context.getString(contentType.titleResId)
val locale = locale.toLocale().getDisplayName(context) val locale = locale.toLocale().getDisplayName(context)
@ -63,6 +65,7 @@ fun MangaSource.getSummary(context: Context): String? = when (this) {
} }
fun MangaSource.getTitle(context: Context): String = when (this) { fun MangaSource.getTitle(context: Context): String = when (this) {
is MangaSourceInfo -> mangaSource.getTitle(context)
is MangaParserSource -> title is MangaParserSource -> title
LocalMangaSource -> context.getString(R.string.local_storage) LocalMangaSource -> context.getString(R.string.local_storage)
else -> context.getString(R.string.unknown) else -> context.getString(R.string.unknown)

@ -0,0 +1,9 @@
package org.koitharu.kotatsu.core.model
import org.koitharu.kotatsu.parsers.model.MangaSource
data class MangaSourceInfo(
val mangaSource: MangaSource,
val isEnabled: Boolean,
val isPinned: Boolean,
) : MangaSource by mangaSource

@ -12,7 +12,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
private val bounds = Rect() private val bounds = Rect()
private val boundsF = RectF() private val boundsF = RectF()
protected val selection = HashSet<Long>() protected val selection = HashSet<Long>() // TODO MutableLongSet
protected var hasBackground: Boolean = true protected var hasBackground: Boolean = true
protected var hasForeground: Boolean = false protected var hasForeground: Boolean = false

@ -14,6 +14,7 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.model.MangaSourceInfo
import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@ -146,7 +147,7 @@ class MangaSourcesRepository @Inject constructor(
}.distinctUntilChanged().onStart { assimilateNewSources() } }.distinctUntilChanged().onStart { assimilateNewSources() }
} }
fun observeEnabledSources(): Flow<List<MangaSource>> = combine( fun observeEnabledSources(): Flow<List<MangaSourceInfo>> = combine(
observeIsNsfwDisabled(), observeIsNsfwDisabled(),
observeSortOrder(), observeSortOrder(),
) { skipNsfw, order -> ) { skipNsfw, order ->
@ -295,23 +296,25 @@ class MangaSourcesRepository @Inject constructor(
private fun List<MangaSourceEntity>.toSources( private fun List<MangaSourceEntity>.toSources(
skipNsfwSources: Boolean, skipNsfwSources: Boolean,
sortOrder: SourcesSortOrder?, sortOrder: SourcesSortOrder?,
): MutableList<MangaSource> { ): MutableList<MangaSourceInfo> {
val result = ArrayList<MangaSource>(size) val result = ArrayList<MangaSourceInfo>(size)
val pinned = HashSet<MangaSource>()
for (entity in this) { for (entity in this) {
val source = entity.source.toMangaSourceOrNull() ?: continue val source = entity.source.toMangaSourceOrNull() ?: continue
if (skipNsfwSources && source.isNsfw()) { if (skipNsfwSources && source.isNsfw()) {
continue continue
} }
if (source in remoteSources) { if (source in remoteSources) {
result.add(source) result.add(
if (entity.isPinned) { MangaSourceInfo(
pinned.add(source) mangaSource = source,
} isEnabled = entity.isEnabled,
isPinned = entity.isPinned,
),
)
} }
} }
if (sortOrder == SourcesSortOrder.ALPHABETIC) { if (sortOrder == SourcesSortOrder.ALPHABETIC) {
result.sortWith(compareBy<MangaSource> { it in pinned }.thenBy { it.getTitle(context) }) result.sortWith(compareBy<MangaSourceInfo> { it.isPinned }.thenBy { it.getTitle(context) })
} }
return result return result
} }

@ -41,8 +41,6 @@ import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
@ -166,16 +164,17 @@ class ExploreFragment :
} }
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean { override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
val isSingleSelection = controller.count == 1 val selectedSources = viewModel.sourcesSnapshot(controller.peekCheckedIds())
val isSingleSelection = selectedSources.size == 1
menu.findItem(R.id.action_settings).isVisible = isSingleSelection menu.findItem(R.id.action_settings).isVisible = isSingleSelection
menu.findItem(R.id.action_shortcut).isVisible = isSingleSelection menu.findItem(R.id.action_shortcut).isVisible = isSingleSelection
menu.findItem(R.id.action_pin).isVisible = selectedSources.all { !it.isPinned }
menu.findItem(R.id.action_unpin).isVisible = selectedSources.all { it.isPinned }
return super.onPrepareActionMode(controller, mode, menu) return super.onPrepareActionMode(controller, mode, menu)
} }
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean { override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
val selectedSources = controller.peekCheckedIds().mapNotNullToSet { id -> val selectedSources = viewModel.sourcesSnapshot(controller.peekCheckedIds())
MangaParserSource.entries.getOrNull(id.toInt()) // TODO
}
if (selectedSources.isEmpty()) { if (selectedSources.isEmpty()) {
return false return false
} }

@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSourceInfo
import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
@ -108,7 +109,7 @@ class ExploreViewModel @Inject constructor(
} }
} }
fun setSourcesPinned(sources: Set<MangaSource>, isPinned: Boolean) { fun setSourcesPinned(sources: Collection<MangaSource>, isPinned: Boolean) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
sourcesRepository.setIsPinned(sources, isPinned) sourcesRepository.setIsPinned(sources, isPinned)
val message = if (sources.size == 1) { val message = if (sources.size == 1) {
@ -125,6 +126,12 @@ class ExploreViewModel @Inject constructor(
settings.closeTip(TIP_SUGGESTIONS) settings.closeTip(TIP_SUGGESTIONS)
} }
fun sourcesSnapshot(ids: Set<Long>): List<MangaSourceInfo> {
return content.value.mapNotNull {
(it as? MangaSourceItem)?.takeIf { x -> x.id in ids }?.source
}
}
private fun createContentFlow() = combine( private fun createContentFlow() = combine(
sourcesRepository.observeEnabledSources(), sourcesRepository.observeEnabledSources(),
getSuggestionFlow(), getSuggestionFlow(),
@ -136,7 +143,7 @@ class ExploreViewModel @Inject constructor(
}.withErrorHandling() }.withErrorHandling()
private fun buildList( private fun buildList(
sources: List<MangaSource>, sources: List<MangaSourceInfo>,
recommendation: List<Manga>, recommendation: List<Manga>,
isGrid: Boolean, isGrid: Boolean,
randomLoading: Boolean, randomLoading: Boolean,

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.explore.ui.adapter package org.koitharu.kotatsu.explore.ui.adapter
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@ -15,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.recyclerView import org.koitharu.kotatsu.core.util.ext.recyclerView
@ -116,6 +118,7 @@ fun exploreSourceListItemAD(
) { ) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener) val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
val iconPinned = ContextCompat.getDrawable(context, R.drawable.ic_pin_small)
binding.root.setOnClickListener(eventListener) binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener) binding.root.setOnLongClickListener(eventListener)
@ -123,6 +126,7 @@ fun exploreSourceListItemAD(
bind { bind {
binding.textViewTitle.text = item.source.getTitle(context) binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewTitle.drawableStart = if (item.source.isPinned) iconPinned else null
binding.textViewSubtitle.text = item.source.getSummary(context) binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
@ -151,6 +155,7 @@ fun exploreSourceGridItemAD(
) { ) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener) val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
val iconPinned = ContextCompat.getDrawable(context, R.drawable.ic_pin_small)
binding.root.setOnClickListener(eventListener) binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener) binding.root.setOnLongClickListener(eventListener)
@ -158,6 +163,7 @@ fun exploreSourceGridItemAD(
bind { bind {
binding.textViewTitle.text = item.source.getTitle(context) binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewTitle.drawableStart = if (item.source.isPinned) iconPinned else null
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name) val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon) fallback(fallbackIcon)

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.explore.ui.model package org.koitharu.kotatsu.explore.ui.model
import org.koitharu.kotatsu.core.model.MangaSourceInfo
import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaSource
data class MangaSourceItem( data class MangaSourceItem(
val source: MangaSource, val source: MangaSourceInfo,
val isGrid: Boolean, val isGrid: Boolean,
) : ListModel { ) : ListModel {

@ -27,15 +27,17 @@
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/list_spacing" android:layout_marginTop="@dimen/list_spacing"
android:drawablePadding="1dp"
android:elegantTextHeight="false" android:elegantTextHeight="false"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:singleLine="true" android:singleLine="true"
android:textAlignment="center" android:textAlignment="center"
android:textAppearance="?attr/textAppearanceBodyMedium" android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[2]" /> tools:drawableStart="@drawable/ic_pin_small"
tools:text="@tools:sample/lorem[0]" />
</LinearLayout> </LinearLayout>

@ -37,9 +37,11 @@
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="2dp"
android:ellipsize="end" android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
tools:drawableStart="@drawable/ic_pin_small"
tools:text="@tools:sample/lorem[2]" /> tools:text="@tools:sample/lorem[2]" />
<TextView <TextView

@ -9,12 +9,6 @@
android:title="@string/disable" android:title="@string/disable"
app:showAsAction="ifRoom|withText" /> app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_shortcut"
android:icon="@drawable/ic_shortcut"
android:title="@string/create_shortcut"
app:showAsAction="ifRoom|withText" />
<item <item
android:id="@+id/action_pin" android:id="@+id/action_pin"
android:icon="@drawable/ic_pin" android:icon="@drawable/ic_pin"
@ -27,6 +21,12 @@
android:title="@string/unpin" android:title="@string/unpin"
app:showAsAction="ifRoom|withText" /> app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_shortcut"
android:icon="@drawable/ic_shortcut"
android:title="@string/create_shortcut"
app:showAsAction="ifRoom|withText" />
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:icon="@drawable/ic_settings" android:icon="@drawable/ic_settings"

Loading…
Cancel
Save