diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt index f4448baa4..f885ddb36 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt @@ -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( diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt index 65b289971..21f57cf3d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt @@ -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(), diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt index c8089e431..1319cf991 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt @@ -9,6 +9,7 @@ class ShelfContent( val favourites: Map>, val updated: Map, val local: List, + val suggestions: List, ) { 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 } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt index 7ed1c3161..1cbb66dd0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt @@ -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, ) { @@ -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> { diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt index de24a9583..d798c09e5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt @@ -2,5 +2,5 @@ package org.koitharu.kotatsu.shelf.domain enum class ShelfSection { - HISTORY, LOCAL, UPDATED, FAVORITES; + HISTORY, LOCAL, UPDATED, FAVORITES, SUGGESTIONS; } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt index b07f64bab..0cf72a0a9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt @@ -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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt index 1ee3f798c..4dfa746ee 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt index 1da0e3f87..27e06aa49 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt @@ -58,10 +58,11 @@ class ShelfViewModel @Inject constructor( val content: LiveData> = 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, isNetworkAvailable: Boolean, ): List { @@ -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, + suggestions: List, + ) { + 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, favourites: Map>, diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt index 73de642e6..4b73dc9ef 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt @@ -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 } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt index da3f0ba2c..168087d10 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt @@ -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, + 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 { diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt index 0f80321a0..cb77f7a68 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt @@ -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> + @Transaction + @Query("SELECT * FROM suggestions ORDER BY relevance DESC LIMIT :limit") + abstract fun observeAll(limit: Int): Flow> + @Query("SELECT COUNT(*) FROM suggestions") abstract suspend fun count(): Int @@ -28,4 +37,4 @@ abstract class SuggestionDao { insert(entity) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt index 50cd7c7f9..14c4561d8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt @@ -22,6 +22,12 @@ class SuggestionRepository @Inject constructor( } } + fun observeAll(limit: Int): Flow> { + return db.suggestionDao.observeAll(limit).mapItems { + it.manga.toManga(it.tags.toMangaTags()) + } + } + suspend fun clear() { db.suggestionDao.deleteAll() } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt b/app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt index 38a82cfe8..b0c426cc2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt @@ -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() } diff --git a/app/src/main/res/drawable/ic_action_pause.xml b/app/src/main/res/drawable/ic_action_pause.xml index 8e5ee878f..147cc6322 100644 --- a/app/src/main/res/drawable/ic_action_pause.xml +++ b/app/src/main/res/drawable/ic_action_pause.xml @@ -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"> Options Content not found or removed Downloading manga - <b>%1$s</b> %2$s + %1$s ยท %2$s Incognito mode Application update available: %s No chapters