Skip to content

Commit

Permalink
Ability to pin manga sources (close #830, close #531)
Browse files Browse the repository at this point in the history
  • Loading branch information
Koitharu committed Jul 7, 2024
1 parent 5ab7e58 commit 9b3ce4d
Show file tree
Hide file tree
Showing 18 changed files with 149 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import org.koitharu.kotatsu.explore.data.SourcesSortOrder
@Dao
abstract class MangaSourcesDao {

@Query("SELECT * FROM sources ORDER BY sort_key")
@Query("SELECT * FROM sources ORDER BY pinned DESC, sort_key")
abstract suspend fun findAll(): List<MangaSourceEntity>

@Query("SELECT source FROM sources WHERE enabled = 1")
Expand All @@ -27,7 +27,7 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources WHERE added_in >= :version")
abstract suspend fun findAllFromVersion(version: Int): List<MangaSourceEntity>

@Query("SELECT * FROM sources ORDER BY sort_key")
@Query("SELECT * FROM sources ORDER BY pinned DESC, sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>>

@Query("SELECT enabled FROM sources WHERE source = :source")
Expand Down Expand Up @@ -55,6 +55,9 @@ abstract class MangaSourcesDao {
@Upsert
abstract suspend fun upsert(entry: MangaSourceEntity)

@Query("SELECT * FROM sources WHERE pinned = 1")
abstract suspend fun findAllPinned(): List<MangaSourceEntity>

fun observeEnabled(order: SourcesSortOrder): Flow<List<MangaSourceEntity>> {
val orderBy = getOrderBy(order)

Expand All @@ -67,7 +70,7 @@ abstract class MangaSourcesDao {
val orderBy = getOrderBy(order)

@Language("RoomSql")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
return findAllImpl(query)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ class MangaSourcesRepository @Inject constructor(
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
}

suspend fun getPinnedSources(): Set<MangaSource> {
assimilateNewSources()
val skipNsfw = settings.isNsfwContentDisabled
return dao.findAllPinned().mapNotNullTo(EnumSet.noneOf(MangaSource::class.java)) {
it.source.toMangaSourceOrNull()?.takeUnless { x -> skipNsfw && x.isNsfw() }
}
}

suspend fun getDisabledSources(): Set<MangaSource> {
assimilateNewSources()
val result = EnumSet.copyOf(remoteSources)
Expand Down Expand Up @@ -226,8 +234,11 @@ class MangaSourcesRepository @Inject constructor(
return settings.sourcesVersion == 0 && dao.findAllEnabledNames().isEmpty()
}

suspend fun setIsPinned(source: MangaSource, isPinned: Boolean) {
dao.setPinned(source.name, isPinned)
suspend fun setIsPinned(sources: Collection<MangaSource>, isPinned: Boolean): ReversibleHandle {
setSourcesPinnedImpl(sources, isPinned)
return ReversibleHandle {
setSourcesEnabledImpl(sources, !isPinned)
}
}

suspend fun trackUsage(source: MangaSource) {
Expand All @@ -246,6 +257,18 @@ class MangaSourcesRepository @Inject constructor(
}
}

private suspend fun setSourcesPinnedImpl(sources: Collection<MangaSource>, isPinned: Boolean) {
if (sources.size == 1) { // fast path
dao.setPinned(sources.first().name, isPinned)
return
}
db.withTransaction {
for (source in sources) {
dao.setPinned(source.name, isPinned)
}
}
}

private suspend fun getNewSources(): MutableSet<MangaSource> {
val entities = dao.findAll()
val result = EnumSet.copyOf(remoteSources)
Expand All @@ -260,17 +283,21 @@ class MangaSourcesRepository @Inject constructor(
sortOrder: SourcesSortOrder?,
): MutableList<MangaSource> {
val result = ArrayList<MangaSource>(size)
val pinned = EnumSet.noneOf(MangaSource::class.java)
for (entity in this) {
val source = entity.source.toMangaSourceOrNull() ?: continue
if (skipNsfwSources && source.isNsfw()) {
continue
}
if (source in remoteSources) {
result.add(source)
if (entity.isPinned) {
pinned.add(source)
}
}
}
if (sortOrder == SourcesSortOrder.ALPHABETIC) {
result.sortBy { it.title }
result.sortWith(compareBy<MangaSource> { it in pinned }.thenBy { it.title })
}
return result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ class ExploreFragment :
mode.finish()
}

R.id.action_pin -> {
viewModel.setSourcesPinned(selectedSources, isPinned = true)
mode.finish()
}

R.id.action_unpin -> {
viewModel.setSourcesPinned(selectedSources, isPinned = false)
mode.finish()
}

else -> return false
}
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ class ExploreViewModel @Inject constructor(
}
}

fun setSourcesPinned(sources: Set<MangaSource>, isPinned: Boolean) {
launchJob(Dispatchers.Default) {
sourcesRepository.setIsPinned(sources, isPinned)
val message = if (sources.size == 1) {
if (isPinned) R.string.source_pinned else R.string.source_unpinned
} else {
if (isPinned) R.string.sources_pinned else R.string.sources_unpinned
}
onActionDone.call(ReversibleAction(message, null))
}
}

fun respondSuggestionTip(isAccepted: Boolean) {
settings.isSuggestionsEnabled = isAccepted
settings.closeTip(TIP_SUGGESTIONS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings.sources.adapter

import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
Expand All @@ -16,49 +17,14 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding
import org.koitharu.kotatsu.databinding.ItemTipBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem

fun sourceConfigItemCheckableDelegate(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
{ layoutInflater, parent ->
ItemSourceConfigCheckableBinding.inflate(
layoutInflater,
parent,
false,
)
},
) {

binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
listener.onItemEnabledChanged(item, isChecked)
}

bind {
binding.textViewTitle.text = item.source.getTitle(context)
binding.switchToggle.isChecked = item.isEnabled
binding.switchToggle.isEnabled = item.isAvailable
binding.textViewDescription.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
crossfade(context)
error(fallbackIcon)
placeholder(fallbackIcon)
fallback(fallbackIcon)
source(item.source)
enqueueWith(coil)
}
}
}

fun sourceConfigItemDelegate2(
listener: SourceConfigListener,
coil: ImageLoader,
Expand All @@ -73,6 +39,7 @@ fun sourceConfigItemDelegate2(
},
) {

val iconPinned = ContextCompat.getDrawable(context, R.drawable.ic_pin_small)
val eventListener = View.OnClickListener { v ->
when (v.id) {
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
Expand All @@ -89,6 +56,7 @@ fun sourceConfigItemDelegate2(
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
binding.imageViewRemove.isVisible = item.isEnabled
binding.imageViewMenu.isVisible = item.isEnabled
binding.textViewTitle.drawableStart = if (item.isPinned) iconPinned else null
binding.textViewDescription.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
Expand Down Expand Up @@ -132,12 +100,15 @@ private fun showSourceMenu(
menu.inflate(R.menu.popup_source_config)
menu.menu.findItem(R.id.action_shortcut)
?.isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(anchor.context)
menu.menu.findItem(R.id.action_pin)?.isVisible = item.isEnabled
menu.menu.findItem(R.id.action_pin)?.isChecked = item.isPinned
menu.menu.findItem(R.id.action_lift)?.isVisible = item.isDraggable
menu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_settings -> listener.onItemSettingsClick(item)
R.id.action_lift -> listener.onItemLiftClick(item)
R.id.action_shortcut -> listener.onItemShortcutClick(item)
R.id.action_pin -> listener.onItemPinClick(item)
}
true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ interface SourceConfigListener : OnTipCloseListener<SourceConfigItem.Tip> {

fun onItemShortcutClick(item: SourceConfigItem.SourceItem)

fun onItemPinClick(item: SourceConfigItem.SourceItem)

fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class SourcesListProducer @Inject constructor(

private suspend fun buildList(): List<SourceConfigItem> {
val enabledSources = repository.getEnabledSources()
val pinned = repository.getPinnedSources()
val isNsfwDisabled = settings.isNsfwContentDisabled
val isReorderAvailable = settings.sourcesSortOrder == SourcesSortOrder.MANUAL
val withTip = isReorderAvailable && settings.isTipEnabled(TIP_REORDER)
Expand All @@ -75,6 +76,7 @@ class SourcesListProducer @Inject constructor(
isEnabled = it in enabledSet,
isDraggable = false,
isAvailable = !isNsfwDisabled || !it.isNsfw(),
isPinned = it in pinned,
)
}.ifEmpty {
listOf(SourceConfigItem.EmptySearchResult)
Expand All @@ -95,6 +97,7 @@ class SourcesListProducer @Inject constructor(
isEnabled = true,
isDraggable = isReorderAvailable,
isAvailable = false,
isPinned = it in pinned,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ class SourcesManageFragment :
}
}

override fun onItemPinClick(item: SourceConfigItem.SourceItem) {
viewModel.setPinned(item.source, !item.isPinned)
}

override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
viewModel.setEnabled(item.source, isEnabled)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ class SourcesManageViewModel @Inject constructor(

fun canReorder(oldPos: Int, newPos: Int): Boolean {
val snapshot = content.value
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false
return (snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled == true
val oldPosItem = snapshot.getOrNull(oldPos) as? SourceConfigItem.SourceItem ?: return false
val newPosItem = snapshot.getOrNull(newPos) as? SourceConfigItem.SourceItem ?: return false
return oldPosItem.isEnabled && newPosItem.isEnabled && oldPosItem.isPinned == newPosItem.isPinned
}

fun setEnabled(source: MangaSource, isEnabled: Boolean) {
Expand All @@ -71,6 +72,14 @@ class SourcesManageViewModel @Inject constructor(
}
}

fun setPinned(source: MangaSource, isPinned: Boolean) {
launchJob(Dispatchers.Default) {
val rollback = repository.setIsPinned(setOf(source), isPinned)
val message = if (isPinned) R.string.source_pinned else R.string.source_unpinned
onActionDone.call(ReversibleAction(message, rollback))
}
}

fun bringToTop(source: MangaSource) {
val snapshot = content.value
launchJob(Dispatchers.Default) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ sealed interface SourceConfigItem : ListModel {
val isEnabled: Boolean,
val isDraggable: Boolean,
val isAvailable: Boolean,
val isPinned: Boolean,
) : SourceConfigItem {

val isNsfw: Boolean
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/res/drawable/ic_pin_small.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
</vector>
2 changes: 1 addition & 1 deletion app/src/main/res/drawable/ic_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:fillColor="#000000"
android:pathData="M19.43 12.98c0.04-0.32 0.07-0.64 0.07-0.98 0-0.34-0.03-0.66-0.07-0.98l2.11-1.65c0.19-0.15 0.24-0.42 0.12-0.64l-2-3.46c-0.09-0.16-0.26-0.25-0.44-0.25-0.06 0-0.12 0.01-0.17 0.03l-2.49 1c-0.52-0.4-1.08-0.73-1.69-0.98l-0.38-2.65C14.46 2.18 14.25 2 14 2h-4C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05l-2.49-1C4.89 5.03 4.83 5.02 4.77 5.02c-0.17 0-0.34 0.09-0.43 0.25l-2 3.46C2.21 8.95 2.27 9.22 2.46 9.37l2.11 1.65C4.53 11.34 4.5 11.67 4.5 12c0 0.33 0.03 0.66 0.07 0.98l-2.11 1.65c-0.19 0.15-0.24 0.42-0.12 0.64l2 3.46c0.09 0.16 0.26 0.25 0.44 0.25 0.06 0 0.12-0.01 0.17-0.03l2.49-1c0.52 0.4 1.08 0.73 1.69 0.98l0.38 2.65C9.54 21.82 9.75 22 10 22h4c0.25 0 0.46-0.18 0.49-0.42l0.38-2.65c0.61-0.25 1.17-0.59 1.69-0.98l2.49 1c0.06 0.02 0.12 0.03 0.18 0.03 0.17 0 0.34-0.09 0.43-0.25l2-3.46c0.12-0.22 0.07-0.49-0.12-0.64l-2.11-1.65zm-1.98-1.71c0.04 0.31 0.05 0.52 0.05 0.73 0 0.21-0.02 0.43-0.05 0.73l-0.14 1.13 0.89 0.7 1.08 0.84-0.7 1.21-1.27-0.51-1.04-0.42-0.9 0.68c-0.43 0.32-0.84 0.56-1.25 0.73l-1.06 0.43-0.16 1.13L12.7 20h-1.4l-0.19-1.35-0.16-1.13-1.06-0.43c-0.43-0.18-0.83-0.41-1.23-0.71l-0.91-0.7-1.06 0.43-1.27 0.51-0.7-1.21 1.08-0.84 0.89-0.7-0.14-1.13C6.52 12.43 6.5 12.2 6.5 12s0.02-0.43 0.05-0.73l0.14-1.13-0.89-0.7L4.72 8.6l0.7-1.21L6.69 7.9l1.04 0.42 0.9-0.68c0.43-0.32 0.84-0.56 1.25-0.73l1.06-0.43 0.16-1.13L11.3 4h1.39l0.19 1.35 0.16 1.13 1.06 0.43c0.43 0.18 0.83 0.41 1.23 0.71l0.91 0.7 1.06-0.43 1.27-0.51 0.7 1.21-1.07 0.85-0.89 0.7 0.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z" />
</vector>
12 changes: 12 additions & 0 deletions app/src/main/res/drawable/ic_shortcut.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M14,14.5V12H10V15H8V11A1,1 0 0,1 9,10H14V7.5L17.5,11M21.71,11.29L12.71,2.29H12.7C12.31,1.9 11.68,1.9 11.29,2.29L2.29,11.29C1.9,11.68 1.9,12.32 2.29,12.71L11.29,21.71C11.68,22.09 12.31,22.1 12.71,21.71L21.71,12.71C22.1,12.32 22.1,11.68 21.71,11.29Z" />
</vector>
12 changes: 12 additions & 0 deletions app/src/main/res/drawable/ic_unpin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M8,6.2V4H7V2H17V4H16V12L18,14V16H17.8L14,12.2V4H10V8.2L8,6.2M20,20.7L18.7,22L12.8,16.1V22H11.2V16H6V14L8,12V11.3L2,5.3L3.3,4L20,20.7M8.8,14H10.6L9.7,13.1L8.8,14Z" />
</vector>
2 changes: 2 additions & 0 deletions app/src/main/res/layout/item_source_config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:drawableStart="@drawable/ic_pin_small"
tools:text="@tools:sample/lorem[15]" />

<TextView
Expand Down
14 changes: 13 additions & 1 deletion app/src/main/res/menu/mode_source.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,22 @@

<item
android:id="@+id/action_shortcut"
android:icon="@drawable/ic_pin"
android:icon="@drawable/ic_shortcut"
android:title="@string/create_shortcut"
app:showAsAction="ifRoom|withText" />

<item
android:id="@+id/action_pin"
android:icon="@drawable/ic_pin"
android:title="@string/pin"
app:showAsAction="ifRoom|withText" />

<item
android:id="@+id/action_unpin"
android:icon="@drawable/ic_unpin"
android:title="@string/unpin"
app:showAsAction="ifRoom|withText" />

<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings"
Expand Down
Loading

0 comments on commit 9b3ce4d

Please sign in to comment.