Show suggestions on the shelf

This commit is contained in:
Koitharu
2023-05-11 16:47:36 +03:00
parent 4d8da40885
commit 26a7a7a2e8
16 changed files with 122 additions and 44 deletions

View File

@@ -5,8 +5,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
@@ -20,6 +18,8 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import java.io.File
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@@ -52,6 +52,7 @@ class AppBackupAgentTest {
title = SampleData.favouriteCategory.title,
sortOrder = SampleData.favouriteCategory.order,
isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled,
isVisibleOnShelf = SampleData.favouriteCategory.isVisibleInLibrary,
)
favouritesRepository.addToCategory(categoryId = category.id, mangas = listOf(SampleData.manga))
historyRepository.addOrUpdate(

View File

@@ -11,7 +11,6 @@ import android.text.format.DateUtils
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.work.WorkManager
import coil.ImageLoader
@@ -155,8 +154,7 @@ class DownloadNotificationFactory @AssistedInject constructor(
null
}
if (state.error != null) {
builder.setContentText(state.error)
builder.setSubText(percent)
builder.setContentText(context.getString(R.string.download_summary_pattern, percent, state.error))
} else {
builder.setContentText(percent)
}
@@ -183,22 +181,7 @@ class DownloadNotificationFactory @AssistedInject constructor(
else -> {
builder.setProgress(state.max, state.progress, false)
val percent = if (state.percent >= 0f) {
context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
} else {
null
}
if (state.eta > 0L) {
val eta = DateUtils.getRelativeTimeSpanString(
state.eta,
System.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS,
)
builder.setContentText(eta)
builder.setSubText(percent)
} else {
builder.setContentText(percent)
}
builder.setContentText(getProgressString(state.percent, state.eta))
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
builder.setStyle(null)
builder.setOngoing(true)
@@ -209,6 +192,29 @@ class DownloadNotificationFactory @AssistedInject constructor(
return builder.build()
}
private fun getProgressString(percent: Float, eta: Long): CharSequence? {
val percentString = if (percent >= 0f) {
context.getString(R.string.percent_string_pattern, (percent * 100).format())
} else {
null
}
val etaString = if (eta > 0L) {
DateUtils.getRelativeTimeSpanString(
eta,
System.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS,
)
} else {
null
}
return when {
percentString == null && etaString == null -> null
percentString != null && etaString == null -> percentString
percentString == null && etaString != null -> etaString
else -> context.getString(R.string.download_summary_pattern, percentString, etaString)
}
}
private fun createMangaIntent(context: Context, manga: Manga?) = PendingIntentCompat.getActivity(
context,
manga.hashCode(),

View File

@@ -9,6 +9,7 @@ class ShelfContent(
val favourites: Map<FavouriteCategory, List<Manga>>,
val updated: Map<Manga, Int>,
val local: List<Manga>,
val suggestions: List<Manga>,
) {
override fun equals(other: Any?): Boolean {
@@ -21,8 +22,7 @@ class ShelfContent(
if (favourites != other.favourites) return false
if (updated != other.updated) return false
if (local != other.local) return false
return true
return suggestions == other.suggestions
}
override fun hashCode(): Int {
@@ -30,6 +30,7 @@ class ShelfContent(
result = 31 * result + favourites.hashCode()
result = 31 * result + updated.hashCode()
result = 31 * result + local.hashCode()
result = 31 * result + suggestions.hashCode()
return result
}
}

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
@@ -34,6 +35,7 @@ class ShelfRepository @Inject constructor(
private val localMangaRepository: LocalMangaRepository,
private val historyRepository: HistoryRepository,
private val trackingRepository: TrackingRepository,
private val suggestionRepository: SuggestionRepository,
private val db: MangaDatabase,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
) {
@@ -43,8 +45,9 @@ class ShelfRepository @Inject constructor(
observeLocalManga(SortOrder.UPDATED),
observeFavourites(),
trackingRepository.observeUpdatedManga(),
) { history, local, favorites, updated ->
ShelfContent(history, favorites, updated, local)
suggestionRepository.observeAll(16),
) { history, local, favorites, updated, suggestions ->
ShelfContent(history, favorites, updated, local, suggestions)
}
private fun observeLocalManga(sortOrder: SortOrder): Flow<List<Manga>> {

View File

@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.shelf.domain
enum class ShelfSection {
HISTORY, LOCAL, UPDATED, FAVORITES;
HISTORY, LOCAL, UPDATED, FAVORITES, SUGGESTIONS;
}

View File

@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.shelf.ui.adapter.ShelfAdapter
import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity
import org.koitharu.kotatsu.utils.ext.addMenuProvider
import javax.inject.Inject
@@ -118,6 +119,7 @@ class ShelfFragment :
is ShelfSectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category)
is ShelfSectionModel.Updated -> UpdatesActivity.newIntent(view.context)
is ShelfSectionModel.Local -> MangaListActivity.newIntent(view.context, MangaSource.LOCAL)
is ShelfSectionModel.Suggestions -> SuggestionsActivity.newIntent(view.context)
}
startActivity(intent)
}

View File

@@ -43,7 +43,9 @@ class ShelfSelectionCallback(
): Boolean {
val checkedIds = controller.peekCheckedIds().entries
val singleKey = checkedIds.singleOrNull { (_, ids) -> ids.isNotEmpty() }?.key
menu.findItem(R.id.action_remove)?.isVisible = singleKey != null && singleKey !is ShelfSectionModel.Updated
menu.findItem(R.id.action_remove)?.isVisible = singleKey != null &&
singleKey !is ShelfSectionModel.Updated &&
singleKey !is ShelfSectionModel.Suggestions
menu.findItem(R.id.action_save)?.isVisible = singleKey !is ShelfSectionModel.Local
return super.onPrepareActionMode(controller, mode, menu)
}
@@ -82,6 +84,8 @@ class ShelfSelectionCallback(
showDeletionConfirm(ids, mode)
return true
}
is ShelfSectionModel.Suggestions -> return false
}
mode.finish()
true

View File

@@ -58,10 +58,11 @@ class ShelfViewModel @Inject constructor(
val content: LiveData<List<ListModel>> = combine(
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
settings.observeAsFlow(AppSettings.KEY_SUGGESTIONS) { isSuggestionsEnabled },
networkState,
repository.observeShelfContent(),
) { sections, isTrackerEnabled, isConnected, content ->
mapList(content, isTrackerEnabled, sections, isConnected)
) { sections, isTrackerEnabled, isSuggestionsEnabled, isConnected, content ->
mapList(content, isTrackerEnabled, isSuggestionsEnabled, sections, isConnected)
}.catch { e ->
emit(listOf(e.toErrorState(canRetry = false)))
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
@@ -157,6 +158,7 @@ class ShelfViewModel @Inject constructor(
private suspend fun mapList(
content: ShelfContent,
isTrackerEnabled: Boolean,
isSuggestionsEnabled: Boolean,
sections: List<ShelfSection>,
isNetworkAvailable: Boolean,
): List<ListModel> {
@@ -171,6 +173,9 @@ class ShelfViewModel @Inject constructor(
}
ShelfSection.FAVORITES -> mapFavourites(result, content.favourites)
ShelfSection.SUGGESTIONS -> if (isSuggestionsEnabled) {
mapSuggestions(result, content.suggestions)
}
}
}
} else {
@@ -190,6 +195,7 @@ class ShelfViewModel @Inject constructor(
ShelfSection.LOCAL -> mapLocal(result, content.local)
ShelfSection.UPDATED -> Unit
ShelfSection.FAVORITES -> Unit
ShelfSection.SUGGESTIONS -> Unit
}
}
}
@@ -257,6 +263,19 @@ class ShelfViewModel @Inject constructor(
)
}
private suspend fun mapSuggestions(
destination: MutableList<in ShelfSectionModel.Suggestions>,
suggestions: List<Manga>,
) {
if (suggestions.isEmpty()) {
return
}
destination += ShelfSectionModel.Suggestions(
items = suggestions.toUi(ListMode.GRID, this, null),
showAllButtonText = R.string.show_all,
)
}
private suspend fun mapFavourites(
destination: MutableList<in ShelfSectionModel.Favourites>,
favourites: Map<FavouriteCategory, List<Manga>>,

View File

@@ -73,4 +73,5 @@ private val ShelfSection.titleResId: Int
ShelfSection.LOCAL -> R.string.local_storage
ShelfSection.UPDATED -> R.string.updated
ShelfSection.FAVORITES -> R.string.favourites
ShelfSection.SUGGESTIONS -> R.string.suggestions
}

View File

@@ -35,9 +35,7 @@ sealed interface ShelfSectionModel : ListModel {
other as History
if (showAllButtonText != other.showAllButtonText) return false
if (items != other.items) return false
return true
return items == other.items
}
override fun hashCode(): Int {
@@ -67,9 +65,7 @@ sealed interface ShelfSectionModel : ListModel {
if (category != other.category) return false
if (showAllButtonText != other.showAllButtonText) return false
if (items != other.items) return false
return true
return items == other.items
}
override fun hashCode(): Int {
@@ -98,9 +94,7 @@ sealed interface ShelfSectionModel : ListModel {
other as Updated
if (items != other.items) return false
if (showAllButtonText != other.showAllButtonText) return false
return true
return showAllButtonText == other.showAllButtonText
}
override fun hashCode(): Int {
@@ -128,9 +122,35 @@ sealed interface ShelfSectionModel : ListModel {
other as Local
if (items != other.items) return false
if (showAllButtonText != other.showAllButtonText) return false
return showAllButtonText == other.showAllButtonText
}
return true
override fun hashCode(): Int {
var result = items.hashCode()
result = 31 * result + showAllButtonText
return result
}
override fun toString(): String = key
}
class Suggestions(
override val items: List<MangaItemModel>,
override val showAllButtonText: Int,
) : ShelfSectionModel {
override val key = "suggestions"
override fun getTitle(resources: Resources) = resources.getString(R.string.suggestions)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Suggestions
if (items != other.items) return false
return showAllButtonText == other.showAllButtonText
}
override fun hashCode(): Int {

View File

@@ -1,6 +1,11 @@
package org.koitharu.kotatsu.suggestions.data
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
@@ -10,6 +15,10 @@ abstract class SuggestionDao {
@Query("SELECT * FROM suggestions ORDER BY relevance DESC")
abstract fun observeAll(): Flow<List<SuggestionWithManga>>
@Transaction
@Query("SELECT * FROM suggestions ORDER BY relevance DESC LIMIT :limit")
abstract fun observeAll(limit: Int): Flow<List<SuggestionWithManga>>
@Query("SELECT COUNT(*) FROM suggestions")
abstract suspend fun count(): Int
@@ -28,4 +37,4 @@ abstract class SuggestionDao {
insert(entity)
}
}
}
}

View File

@@ -22,6 +22,12 @@ class SuggestionRepository @Inject constructor(
}
}
fun observeAll(limit: Int): Flow<List<Manga>> {
return db.suggestionDao.observeAll(limit).mapItems {
it.manga.toManga(it.tags.toMangaTags())
}
}
suspend fun clear() {
db.suggestionDao.deleteAll()
}

View File

@@ -10,6 +10,8 @@ import androidx.work.impl.foreground.SystemForegroundService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
@@ -27,8 +29,10 @@ class WorkServiceStopHelper(
WorkManager.getInstance(context)
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
.asFlow()
.map { it.isEmpty() }
.distinctUntilChanged()
.collectLatest {
if (it.isEmpty()) {
if (it) {
delay(1_000)
stopWorkerService()
}

View File

@@ -3,6 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View File

@@ -3,6 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View File

@@ -361,7 +361,7 @@
<string name="options">Options</string>
<string name="not_found_404">Content not found or removed</string>
<string name="downloading_manga">Downloading manga</string>
<string name="download_summary_pattern" translatable="false">&lt;b&gt;%1$s&lt;/b&gt; %2$s</string>
<string name="download_summary_pattern" translatable="false">%1$s · %2$s</string>
<string name="incognito_mode">Incognito mode</string>
<string name="app_update_available_s">Application update available: %s</string>
<string name="no_chapters">No chapters</string>