From f0e56c4b6a3c41f29671b697e6434c67e1beb18f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 12 May 2021 20:10:47 +0300 Subject: [PATCH 01/23] Suggestions --- .idea/misc.xml | 3 + .../java/org/koitharu/kotatsu/KotatsuApp.kt | 4 +- .../koitharu/kotatsu/core/db/MangaDatabase.kt | 4 ++ .../koitharu/kotatsu/core/prefs/AppSection.kt | 2 +- .../kotatsu/history/data/HistoryDao.kt | 4 ++ .../history/domain/HistoryRepository.kt | 5 ++ .../koitharu/kotatsu/main/ui/MainActivity.kt | 11 +++ .../kotatsu/suggestions/SuggestionsModule.kt | 14 ++++ .../kotatsu/suggestions/data/SuggestionDao.kt | 28 ++++++++ .../data}/SuggestionEntity.kt | 5 +- .../suggestions/data/SuggestionWithManga.kt | 23 +++++++ .../suggestions/domain/MangaSuggestion.kt | 10 +++ .../domain/SuggestionRepository.kt | 38 +++++++++++ .../suggestions/ui/SuggestionsFragment.kt | 28 ++++++++ .../suggestions/ui/SuggestionsViewModel.kt | 53 +++++++++++++++ .../suggestions/ui/SuggestionsWorker.kt | 68 +++++++++++++++++++ app/src/main/res/drawable/ic_suggestion.xml | 12 ++++ app/src/main/res/menu/nav_drawer.xml | 4 ++ app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 20 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/SuggestionsModule.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt rename app/src/main/java/org/koitharu/kotatsu/{core/db/entity => suggestions/data}/SuggestionEntity.kt (77%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt create mode 100644 app/src/main/res/drawable/ic_suggestion.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 7f598c7c9..44afccb0c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -9,6 +9,8 @@ + + @@ -33,6 +35,7 @@ + diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 311a9bdb6..85e7adf5d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.reader.readerModule import org.koitharu.kotatsu.remotelist.remoteListModule import org.koitharu.kotatsu.search.searchModule import org.koitharu.kotatsu.settings.settingsModule +import org.koitharu.kotatsu.suggestions.suggestionsModule import org.koitharu.kotatsu.tracker.trackerModule import org.koitharu.kotatsu.widget.WidgetUpdater import org.koitharu.kotatsu.widget.appWidgetModule @@ -65,7 +66,8 @@ class KotatsuApp : Application() { trackerModule, settingsModule, readerModule, - appWidgetModule + appWidgetModule, + suggestionsModule, ) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index f0844571e..9c40edaae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -10,6 +10,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.FavouritesDao import org.koitharu.kotatsu.history.data.HistoryDao import org.koitharu.kotatsu.history.data.HistoryEntity +import org.koitharu.kotatsu.suggestions.data.SuggestionDao +import org.koitharu.kotatsu.suggestions.data.SuggestionEntity @Database( entities = [ @@ -35,4 +37,6 @@ abstract class MangaDatabase : RoomDatabase() { abstract val tracksDao: TracksDao abstract val trackLogsDao: TrackLogsDao + + abstract val suggestionDao: SuggestionDao } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt index 64ce67264..0efa45c92 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt @@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.prefs enum class AppSection { - LOCAL, FAVOURITES, HISTORY, FEED + LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt index 9ee2642fb..0b973aa64 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.history.data import androidx.room.* import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity @Dao @@ -22,6 +23,9 @@ abstract class HistoryDao { @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)") abstract suspend fun findAllManga(): List + @Query("SELECT * FROM tags WHERE tag_id IN (SELECT tag_id FROM manga_tags WHERE manga_id IN (SELECT manga_id FROM history))") + abstract suspend fun findAllTags(): List + @Query("SELECT * FROM history WHERE manga_id = :id") abstract suspend fun find(id: Long): HistoryEntity? diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt index cece71003..ade87172b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt @@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.mapItems @@ -84,4 +85,8 @@ class HistoryRepository( db.historyDao.delete(manga.id) } } + + suspend fun getAllTags(): List { + return db.historyDao.findAllTags().map { x -> x.toMangaTag() } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 443ab118f..6abc5fae4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -32,6 +32,8 @@ import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.search.ui.SearchHelper import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment +import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -75,6 +77,7 @@ class MainActivity : BaseActivity(), } if (savedInstanceState == null) { TrackWorker.setup(applicationContext) + SuggestionsWorker.setup(applicationContext) AppUpdateChecker(this).launchIfNeeded() } @@ -144,6 +147,10 @@ class MainActivity : BaseActivity(), viewModel.defaultSection = AppSection.LOCAL setPrimaryFragment(LocalListFragment.newInstance()) } + R.id.nav_suggestions -> { + viewModel.defaultSection = AppSection.SUGGESTIONS + setPrimaryFragment(SuggestionsFragment.newInstance()) + } R.id.nav_feed -> { viewModel.defaultSection = AppSection.FEED setPrimaryFragment(FeedFragment.newInstance()) @@ -229,6 +236,10 @@ class MainActivity : BaseActivity(), binding.navigationView.setCheckedItem(R.id.nav_feed) setPrimaryFragment(FeedFragment.newInstance()) } + AppSection.SUGGESTIONS -> { + binding.navigationView.setCheckedItem(R.id.nav_suggestions) + setPrimaryFragment(SuggestionsFragment.newInstance()) + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/SuggestionsModule.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/SuggestionsModule.kt new file mode 100644 index 000000000..df0a2c870 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/SuggestionsModule.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.suggestions + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module +import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository +import org.koitharu.kotatsu.suggestions.ui.SuggestionsViewModel + +val suggestionsModule + get() = module { + + factory { SuggestionRepository(get()) } + + viewModel { SuggestionsViewModel(get(), get()) } + } \ No newline at end of file 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 new file mode 100644 index 000000000..bf5f2f055 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.suggestions.data + +import androidx.room.* +import kotlinx.coroutines.flow.Flow + +@Dao +abstract class SuggestionDao { + + @Transaction + @Query("SELECT * FROM suggestions ORDER BY relevance DESC") + abstract fun observeAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract suspend fun insert(entity: SuggestionEntity): Long + + @Update + abstract suspend fun update(entity: SuggestionEntity): Int + + @Query("DELETE FROM suggestions") + abstract suspend fun deleteAll() + + @Transaction + open suspend fun upsert(entity: SuggestionEntity) { + if (update(entity) == 0) { + insert(entity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/SuggestionEntity.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/SuggestionEntity.kt rename to app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt index 896732e15..174810bc2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/SuggestionEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt @@ -1,9 +1,11 @@ -package org.koitharu.kotatsu.core.db.entity +package org.koitharu.kotatsu.suggestions.data +import androidx.annotation.FloatRange import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.koitharu.kotatsu.core.db.entity.MangaEntity @Entity( tableName = "suggestions", @@ -19,6 +21,7 @@ import androidx.room.PrimaryKey data class SuggestionEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "manga_id", index = true) val mangaId: Long, + @FloatRange(from = 0.0, to = 1.0) @ColumnInfo(name = "relevance") val relevance: Float, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), ) diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt new file mode 100644 index 000000000..13aa11bec --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.suggestions.data + +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity + +data class SuggestionWithManga( + @Embedded val suggestion: SuggestionEntity, + @Relation( + parentColumn = "manga_id", + entityColumn = "manga_id" + ) + val manga: MangaEntity, + @Relation( + parentColumn = "manga_id", + entityColumn = "tag_id", + associateBy = Junction(MangaTagsEntity::class) + ) + val tags: List +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt new file mode 100644 index 000000000..689d8276a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.suggestions.domain + +import androidx.annotation.FloatRange +import org.koitharu.kotatsu.core.model.Manga + +data class MangaSuggestion( + val manga: Manga, + @FloatRange(from = 0.0, to = 1.0) + val relevance: Float, +) \ 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 new file mode 100644 index 000000000..4ea272ba5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt @@ -0,0 +1,38 @@ +package org.koitharu.kotatsu.suggestions.domain + +import androidx.room.withTransaction +import kotlinx.coroutines.flow.Flow +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.suggestions.data.SuggestionEntity +import org.koitharu.kotatsu.utils.ext.mapItems +import org.koitharu.kotatsu.utils.ext.mapToSet + +class SuggestionRepository( + private val db: MangaDatabase, +) { + + fun observeAll(): Flow> { + return db.suggestionDao.observeAll().mapItems { + it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) + } + } + + suspend fun replace(suggestions: Iterable) { + db.withTransaction { + db.suggestionDao.deleteAll() + suggestions.forEach { x -> + db.mangaDao.upsert(MangaEntity.from(x.manga)) + db.suggestionDao.upsert( + SuggestionEntity( + mangaId = x.manga.id, + relevance = x.relevance, + createdAt = System.currentTimeMillis(), + ) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt new file mode 100644 index 000000000..c53b705e5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.suggestions.ui + +import android.os.Bundle +import android.view.View +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.list.ui.MangaListFragment + +class SuggestionsFragment : MangaListFragment() { + + override val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) + override val isSwipeRefreshEnabled = false + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + } + + override fun onScrolledToEnd() = Unit + + override fun getTitle(): CharSequence? { + return context?.getString(R.string.suggestions) + } + + companion object { + + fun newInstance() = SuggestionsFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt new file mode 100644 index 000000000..cc3e52d2f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt @@ -0,0 +1,53 @@ +package org.koitharu.kotatsu.suggestions.ui + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.list.ui.model.* +import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository +import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.onFirst + +class SuggestionsViewModel( + repository: SuggestionRepository, + settings: AppSettings, +) : MangaListViewModel(settings) { + + override val content = combine( + repository.observeAll(), + createListModeFlow() + ) { list, mode -> + when { + list.isEmpty() -> listOf(EmptyState(R.string.text_suggestion_holder)) + else -> mapList(list, mode) + } + }.onFirst { + isLoading.postValue(false) + }.catch { + it.toErrorState(canRetry = false) + }.asLiveDataDistinct( + viewModelScope.coroutineContext + Dispatchers.Default, + listOf(LoadingState) + ) + + override fun onRefresh() = Unit + + override fun onRetry() = Unit + + private fun mapList( + list: List, + mode: ListMode, + ): List = list.map { manga -> + when (mode) { + ListMode.LIST -> manga.toListModel() + ListMode.DETAILED_LIST -> manga.toListDetailedModel() + ListMode.GRID -> manga.toGridModel() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt new file mode 100644 index 000000000..895508592 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -0,0 +1,68 @@ +package org.koitharu.kotatsu.suggestions.ui + +import android.content.Context +import androidx.work.* +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.SortOrder +import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion +import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository +import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf +import java.util.concurrent.TimeUnit +import kotlin.math.pow + +class SuggestionsWorker(appContext: Context, params: WorkerParameters) : + CoroutineWorker(appContext, params), KoinComponent { + + private val suggestionRepository by inject() + private val historyRepository by inject() + + override suspend fun doWork(): Result { + val rawResults = ArrayList() + val allTags = historyRepository.getAllTags() + val tagsBySources = allTags.groupBy { x -> x.source } + for ((source, tags) in tagsBySources) { + val repo = mangaRepositoryOf(source) + tags.flatMapTo(rawResults) { tag -> + repo.getList( + offset = 0, + sortOrder = SortOrder.UPDATED, + tag = tag, + ) + } + } + suggestionRepository.replace( + rawResults.distinctBy { manga -> + manga.id + }.map { manga -> + val jointTags = manga.tags intersect allTags + MangaSuggestion( + manga = manga, + relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(), + ) + } + ) + return Result.success() + } + + companion object { + + private const val TAG = "suggestions" + + fun setup(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresBatteryNotLow(true) + .build() + val request = PeriodicWorkRequestBuilder(6, TimeUnit.HOURS) + .setConstraints(constraints) + .addTag(TAG) + .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) + .build() + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_suggestion.xml b/app/src/main/res/drawable/ic_suggestion.xml new file mode 100644 index 000000000..a93a75799 --- /dev/null +++ b/app/src/main/res/drawable/ic_suggestion.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer.xml b/app/src/main/res/menu/nav_drawer.xml index 75869c7a7..1e0db493c 100644 --- a/app/src/main/res/menu/nav_drawer.xml +++ b/app/src/main/res/menu/nav_drawer.xml @@ -14,6 +14,10 @@ android:id="@+id/nav_history" android:icon="@drawable/ic_history" android:title="@string/history" /> + Enter password that will be required when the application starts Confirm Password must be at least 4 characters + Suggestions + Start reading manga and you will get personalized suggestions \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 161e209c5..fce999bca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -207,4 +207,6 @@ Enter password that will be required when the application starts Confirm Password must be at least 4 characters + Suggestions + Start reading manga and you will get personalized suggestions \ No newline at end of file From 632715e6c9af1c6b1bc6a1770cc02ea6ea4827ab Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 27 Feb 2022 18:25:18 +0200 Subject: [PATCH 02/23] Suggestions settings --- .../kotatsu/core/prefs/AppSettings.kt | 8 +++ .../history/domain/HistoryRepository.kt | 4 +- .../koitharu/kotatsu/main/ui/MainActivity.kt | 16 ++++- .../koitharu/kotatsu/main/ui/MainViewModel.kt | 6 ++ .../kotatsu/settings/MainSettingsFragment.kt | 20 ++++-- .../settings/SuggestionsSettingsFragment.kt | 46 ++++++++++++++ .../kotatsu/suggestions/data/SuggestionDao.kt | 3 + .../domain/SuggestionRepository.kt | 8 +++ .../suggestions/ui/SuggestionsFragment.kt | 2 +- .../suggestions/ui/SuggestionsWorker.kt | 62 +++++++++++++++---- .../kotatsu/tracker/work/TrackWorker.kt | 4 +- app/src/main/res/values-ru/strings.xml | 8 +++ app/src/main/res/values/strings.xml | 10 ++- app/src/main/res/xml/pref_main.xml | 9 ++- app/src/main/res/xml/pref_suggestions.xml | 27 ++++++++ 15 files changed, 204 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt create mode 100644 app/src/main/res/xml/pref_suggestions.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 17b8adeeb..a0e9dbc00 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -135,6 +135,12 @@ class AppSettings(context: Context) { } } + val isSuggestionsEnabled: Boolean + get() = prefs.getBoolean(KEY_SUGGESTIONS, false) + + val isSuggestionsExcludeNsfw: Boolean + get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false) + fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat = when (format) { "" -> DateFormat.getDateInstance(DateFormat.SHORT) @@ -224,6 +230,8 @@ class AppSettings(context: Context) { const val KEY_REVERSE_CHAPTERS = "reverse_chapters" const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw" const val KEY_PAGES_NUMBERS = "pages_numbers" + const val KEY_SUGGESTIONS = "suggestions" + const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw" // About const val KEY_APP_UPDATE = "app_update" diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt index e34a4fd92..c492e0e0b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt @@ -91,7 +91,7 @@ class HistoryRepository( } } - suspend fun getAllTags(): List { - return db.historyDao.findAllTags().map { x -> x.toMangaTag() } + suspend fun getAllTags(): Set { + return db.historyDao.findAllTags().mapToSet { x -> x.toMangaTag() } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 6b59897ad..58078b4fe 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -43,12 +43,15 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker -import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.work.TrackWorker -import org.koitharu.kotatsu.utils.ext.* +import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.hideKeyboard +import org.koitharu.kotatsu.utils.ext.measureHeight +import org.koitharu.kotatsu.utils.ext.resolveDp class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener, AppBarOwner, @@ -116,6 +119,7 @@ class MainActivity : BaseActivity(), viewModel.onError.observe(this, this::onError) viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.remoteSources.observe(this, this::updateSideMenu) + viewModel.isSuggestionsEnabled.observe(this, this::setSuggestionsEnabled) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { @@ -301,6 +305,14 @@ class MainActivity : BaseActivity(), submenu.setGroupCheckable(R.id.group_remote_sources, true, true) } + private fun setSuggestionsEnabled(isEnabled: Boolean) { + val item = binding.navigationView.menu.findItem(R.id.nav_suggestions) ?: return + if (!isEnabled && item.isChecked) { + binding.navigationView.setCheckedItem(R.id.nav_history) + } + item.isVisible = isEnabled + } + private fun openDefaultSection() { when (viewModel.defaultSection) { AppSection.LOCAL -> { diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt index ec9566e68..f197e454c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt @@ -21,6 +21,12 @@ class MainViewModel( val onOpenReader = SingleLiveEvent() var defaultSection by settings::defaultSection + val isSuggestionsEnabled = settings.observe() + .filter { it == AppSettings.KEY_SUGGESTIONS } + .onStart { emit("") } + .map { settings.isSuggestionsEnabled } + .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + val remoteSources = settings.observe() .filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN } .onStart { emit("") } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt index 17e8b6a93..7a2b992ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt @@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceScreen -import androidx.preference.SwitchPreference +import androidx.preference.TwoStatePreference import kotlinx.coroutines.launch import leakcanary.LeakCanary import org.koin.android.ext.android.inject @@ -56,7 +56,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), entryValues = ListMode.values().names() setDefaultValueCompat(ListMode.GRID.name) } - findPreference(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = + findPreference(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = AppSettings.isDynamicColorAvailable findPreference(AppSettings.KEY_DATE_FORMAT)?.run { entryValues = arrayOf("", "MM/dd/yy", "dd/MM/yy", "yyyy-MM-dd", "dd MMM yyyy", "MMM dd, yyyy") @@ -72,12 +72,15 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), setDefaultValueCompat("") summary = "%s" } + findPreference(AppSettings.KEY_SUGGESTIONS)?.setSummary( + if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled + ) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) findPreference(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName() - findPreference(AppSettings.KEY_PROTECT_APP)?.isChecked = + findPreference(AppSettings.KEY_PROTECT_APP)?.isChecked = !settings.appPassword.isNullOrEmpty() settings.subscribe(this) } @@ -114,15 +117,20 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), findPreference(key)?.setSummary(R.string.restart_required) } AppSettings.KEY_HIDE_TOOLBAR -> { - findPreference(key)?.setSummary(R.string.restart_required) + findPreference(key)?.setSummary(R.string.restart_required) } AppSettings.KEY_LOCAL_STORAGE -> { findPreference(key)?.bindStorageName() } AppSettings.KEY_APP_PASSWORD -> { - findPreference(AppSettings.KEY_PROTECT_APP) + findPreference(AppSettings.KEY_PROTECT_APP) ?.isChecked = !settings.appPassword.isNullOrEmpty() } + AppSettings.KEY_SUGGESTIONS -> { + findPreference(AppSettings.KEY_SUGGESTIONS)?.setSummary( + if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled + ) + } } } @@ -148,7 +156,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), true } AppSettings.KEY_PROTECT_APP -> { - val pref = (preference as? SwitchPreference ?: return false) + val pref = (preference as? TwoStatePreference ?: return false) if (pref.isChecked) { pref.isChecked = false startActivity(Intent(preference.context, ProtectSetupActivity::class.java)) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt new file mode 100644 index 000000000..02467f1d6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.settings + +import android.content.SharedPreferences +import android.os.Bundle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository +import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker + +class SuggestionsSettingsFragment : BasePreferenceFragment(R.string.suggestions), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val repository by inject(mode = LazyThreadSafetyMode.NONE) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + settings.subscribe(this) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_suggestions) + } + + override fun onDestroy() { + super.onDestroy() + settings.unsubscribe(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + if (key == AppSettings.KEY_SUGGESTIONS && settings.isSuggestionsEnabled) { + onSuggestionsEnabled() + } + } + + private fun onSuggestionsEnabled() { + lifecycleScope.launch { + if (repository.isEmpty()) { + SuggestionsWorker.startNow(context ?: return@launch) + } + } + } +} \ No newline at end of file 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 bf5f2f055..0f80321a0 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 @@ -10,6 +10,9 @@ abstract class SuggestionDao { @Query("SELECT * FROM suggestions ORDER BY relevance DESC") abstract fun observeAll(): Flow> + @Query("SELECT COUNT(*) FROM suggestions") + abstract suspend fun count(): Int + @Insert(onConflict = OnConflictStrategy.IGNORE) abstract suspend fun insert(entity: SuggestionEntity): Long 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 4ea272ba5..8afa1dd3e 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 @@ -20,6 +20,14 @@ class SuggestionRepository( } } + suspend fun clear() { + db.suggestionDao.deleteAll() + } + + suspend fun isEmpty(): Boolean { + return db.suggestionDao.count() == 0 + } + suspend fun replace(suggestions: Iterable) { db.withTransaction { db.suggestionDao.deleteAll() diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt index c53b705e5..b4f61427b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -8,7 +8,7 @@ import org.koitharu.kotatsu.list.ui.MangaListFragment class SuggestionsFragment : MangaListFragment() { - override val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) + override val viewModel by viewModel() override val isSwipeRefreshEnabled = false override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index 3eaeaba29..f602e534d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -6,6 +6,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.SortOrder +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository @@ -18,10 +19,25 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : private val suggestionRepository by inject() private val historyRepository by inject() + private val appSettings by inject() - override suspend fun doWork(): Result { + override suspend fun doWork(): Result = try { + val count = doWorkImpl() + Result.success(workDataOf(DATA_COUNT to count)) + } catch (t: Throwable) { + Result.failure() + } + + private suspend fun doWorkImpl(): Int { + if (!appSettings.isSuggestionsEnabled) { + suggestionRepository.clear() + return 0 + } val rawResults = ArrayList() val allTags = historyRepository.getAllTags() + if (allTags.isEmpty()) { + return 0 + } val tagsBySources = allTags.groupBy { x -> x.source } for ((source, tags) in tagsBySources) { val repo = mangaRepositoryOf(source) @@ -33,23 +49,31 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : ) } } - suggestionRepository.replace( - rawResults.distinctBy { manga -> - manga.id - }.map { manga -> - val jointTags = manga.tags intersect allTags - MangaSuggestion( - manga = manga, - relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(), - ) - } - ) - return Result.success() + if (appSettings.isSuggestionsExcludeNsfw) { + rawResults.removeAll { it.isNsfw } + } + if (rawResults.isEmpty()) { + return 0 + } + val suggestions = rawResults.distinctBy { manga -> + manga.id + }.map { manga -> + val jointTags = manga.tags intersect allTags + MangaSuggestion( + manga = manga, + relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(), + ) + }.sortedBy { it.relevance }.take(LIMIT) + suggestionRepository.replace(suggestions) + return suggestions.size } companion object { private const val TAG = "suggestions" + private const val TAG_ONESHOT = "suggestions_oneshot" + private const val LIMIT = 140 + private const val DATA_COUNT = "count" fun setup(context: Context) { val constraints = Constraints.Builder() @@ -64,5 +88,17 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : WorkManager.getInstance(context) .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) } + + fun startNow(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .addTag(TAG_ONESHOT) + .build() + WorkManager.getInstance(context) + .enqueue(request) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index d345ca5e4..ec47a74b3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -26,7 +26,6 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf import org.koitharu.kotatsu.utils.ext.toBitmapOrNull -import org.koitharu.kotatsu.utils.ext.toUriOrNull import org.koitharu.kotatsu.utils.progress.Progress import java.util.concurrent.TimeUnit @@ -237,6 +236,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : private const val DATA_PROGRESS = "progress" private const val DATA_TOTAL = "total" private const val TAG = "tracking" + private const val TAG_ONESHOT = "tracking_oneshot" @RequiresApi(Build.VERSION_CODES.O) private fun createNotificationChannel(context: Context) { @@ -277,7 +277,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : .build() val request = OneTimeWorkRequestBuilder() .setConstraints(constraints) - .addTag(TAG) + .addTag(TAG_ONESHOT) .build() WorkManager.getInstance(context) .enqueue(request) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c15cb6480..5846cd0f2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -249,4 +249,12 @@ Доступные источники Динамическая тема Применяет тему приложения, основанную на цветовой палитре обоев на устройстве + Рекомендации + Включить рекомендации + Предлагать мангу на основе Ваших предпочтений + Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы + Начните читать мангу, чтобы получать персональные предложения + Не предлагать NSFW мангу + Включено + Выключено \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38a302e0e..f58a73e48 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -251,6 +251,12 @@ Dynamic theme Applies a theme created on the color scheme of your wallpaper Importing manga: %1$d of %2$d - Suggestions - Start reading manga and you will get personalized suggestions + Suggestions + Enable suggestions + Suggest manga based on your preferences + All data is analyzed locally on this device. There is no transfer of your personal data to any services + Start reading manga and you will get personalized suggestions + Do not suggest NSFW manga + Enabled + Disabled \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 84bae3f2a..f6cbe59cf 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -61,6 +61,13 @@ app:allowDividerAbove="true" app:iconSpaceReserved="false" /> + + - + + + + + + + + + \ No newline at end of file From 5d26743c8f539e5a2c142d2309cf42d6f7e95d3b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 28 Feb 2022 19:08:16 +0200 Subject: [PATCH 03/23] Fix tags for suggestions --- .../kotatsu/suggestions/domain/SuggestionRepository.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 8afa1dd3e..aec0a948d 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 @@ -32,7 +32,9 @@ class SuggestionRepository( db.withTransaction { db.suggestionDao.deleteAll() suggestions.forEach { x -> - db.mangaDao.upsert(MangaEntity.from(x.manga)) + val tags = x.manga.tags.map(TagEntity.Companion::fromMangaTag) + db.tagsDao.upsert(tags) + db.mangaDao.upsert(MangaEntity.from(x.manga), tags) db.suggestionDao.upsert( SuggestionEntity( mangaId = x.manga.id, From 27658eea20dd10016fd5580c4cc100344acc5903 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 1 Mar 2022 07:59:15 +0200 Subject: [PATCH 04/23] Fix tags case --- .../koitharu/kotatsu/core/parser/site/AnibelRepository.kt | 1 - .../koitharu/kotatsu/core/parser/site/ChanRepository.kt | 4 ++-- .../koitharu/kotatsu/core/parser/site/DesuMeRepository.kt | 4 ++-- .../kotatsu/core/parser/site/ExHentaiRepository.kt | 4 ++-- .../koitharu/kotatsu/core/parser/site/GroupleRepository.kt | 6 +++--- .../koitharu/kotatsu/core/parser/site/HenChanRepository.kt | 3 ++- .../kotatsu/core/parser/site/MangaDexRepository.kt | 5 +++-- .../kotatsu/core/parser/site/MangaLibRepository.kt | 4 ++-- .../kotatsu/core/parser/site/MangaOwlRepository.kt | 4 ++-- .../kotatsu/core/parser/site/MangaTownRepository.kt | 6 +++--- .../kotatsu/core/parser/site/MangareadRepository.kt | 6 +++--- .../kotatsu/core/parser/site/NineMangaRepository.kt | 2 +- .../koitharu/kotatsu/core/parser/site/RemangaRepository.kt | 6 +++--- .../java/org/koitharu/kotatsu/local/data/MangaIndex.kt | 7 ++----- 14 files changed, 30 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt index e71378eec..beb969daf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt @@ -237,7 +237,6 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor when { c == '-' -> { builder.setCharAt(i, ' ') - capitalize = true } capitalize -> { builder.setCharAt(i, c.uppercaseChar()) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt index 82a0a3268..ceb4ee024 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt @@ -61,7 +61,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe tags = runCatching { row.selectFirst("div.genre")?.select("a")?.mapToSet { MangaTag( - title = it.text(), + title = it.text().toTitleCase(), key = it.attr("href").substringAfterLast('/').urlEncoded(), source = source ) @@ -136,7 +136,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe return root.select("li.sidetag").mapToSet { li -> val a = li.children().last() ?: throw ParseException("a is null") MangaTag( - title = a.text().toCamelCase(), + title = a.text().toTitleCase(), key = a.attr("href").substringAfterLast('/'), source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt index 0b5fd9b65..308f209b0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt @@ -85,7 +85,7 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor tags = json.getJSONArray("genres").mapToSet { MangaTag( key = it.getString("text"), - title = it.getString("russian"), + title = it.getString("russian").toTitleCase(), source = manga.source ) }, @@ -133,7 +133,7 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor MangaTag( source = source, key = it.selectFirst("input")?.attr("data-genre") ?: parseFailed(), - title = it.selectFirst("label")?.text() ?: parseFailed() + title = it.selectFirst("label")?.text()?.toTitleCase() ?: parseFailed() ) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt index 41b86750e..00619c34f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt @@ -85,7 +85,7 @@ class ExHentaiRepository( val tagsDiv = glink.nextElementSibling() ?: parseFailed("tags div not found") val mainTag = td2.selectFirst("div.cn")?.let { div -> MangaTag( - title = div.text(), + title = div.text().toTitleCase(), key = tagIdByClass(div.classNames()) ?: return@let null, source = source, ) @@ -181,7 +181,7 @@ class ExHentaiRepository( val id = div.id().substringAfterLast('_').toIntOrNull() ?: return@mapNotNullToSet null MangaTag( - title = div.text(), + title = div.text().toTitleCase(), key = id.toString(), source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt index 598a43bf0..c1f49d804 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt @@ -89,7 +89,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : tileInfo?.select("a.element-link") ?.mapToSet { MangaTag( - title = it.text(), + title = it.text().toTitleCase(), key = it.attr("href").substringAfterLast('/'), source = source ) @@ -119,7 +119,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : .mapNotNull { val a = it.selectFirst("a.element-link") ?: return@mapNotNull null MangaTag( - title = a.text(), + title = a.text().toTitleCase(), key = a.attr("href").substringAfterLast('/'), source = source ) @@ -183,7 +183,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : ?.selectFirst("table.table") ?: parseFailed("Cannot find root") return root.select("a.element-link").mapToSet { a -> MangaTag( - title = a.text().toCamelCase(), + title = a.text().toTitleCase(), key = a.attr("href").substringAfterLast('/'), source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt index 072c7611b..2c25870b9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt @@ -5,6 +5,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.parseHtml +import org.koitharu.kotatsu.utils.ext.toTitleCase class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) { @@ -36,7 +37,7 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load tags = root.selectFirst("div.sidetags")?.select("li.sidetag")?.mapToSet { val a = it.children().last() ?: parseFailed("Invalid tag") MangaTag( - title = a.text(), + title = a.text().toTitleCase(), key = a.attr("href").substringAfterLast('/'), source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt index 2b289212b..57e57c025 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt @@ -94,7 +94,8 @@ class MangaDexRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit MangaTag( title = tag.getJSONObject("attributes") .getJSONObject("name") - .firstStringValue(), + .firstStringValue() + .toTitleCase(), key = tag.getString("id"), source = source, ) @@ -194,7 +195,7 @@ class MangaDexRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit .getJSONArray("data") return tags.mapToSet { jo -> MangaTag( - title = jo.getJSONObject("attributes").getJSONObject("name").firstStringValue(), + title = jo.getJSONObject("attributes").getJSONObject("name").firstStringValue().toTitleCase(), key = jo.getString("id"), source = source, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt index ed58f073c..8ebbf2c44 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt @@ -139,7 +139,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : tags = info?.selectFirst("div.media-tags") ?.select("a.media-tag-item")?.mapToSet { a -> MangaTag( - title = a.text().toCamelCase(), + title = a.text().toTitleCase(), key = a.attr("href").substringAfterLast('='), source = source ) @@ -203,7 +203,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : result += MangaTag( source = source, key = x.getInt("id").toString(), - title = x.getString("name").toCamelCase() + title = x.getString("name").toTitleCase(), ) } return result diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt index 5e5429d95..a9c0030d2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt @@ -91,7 +91,7 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit .mapNotNull { val a = it.selectFirst("a") ?: return@mapNotNull null MangaTag( - title = a.text(), + title = a.text().toTitleCase(), key = a.attr("href"), source = source ) @@ -144,7 +144,7 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit return root.mapToSet { p -> val a = p.selectFirst("a") ?: parseFailed("a is null") MangaTag( - title = a.text().toCamelCase(), + title = a.text().toTitleCase(), key = a.attr("href"), source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt index afe3750c3..973bb77bb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt @@ -80,7 +80,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : }, tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNullToSet tags@{ x -> MangaTag( - title = x.attr("title"), + title = x.attr("title").toTitleCase(), key = x.attr("href").parseTagKey() ?: return@tags null, source = MangaSource.MANGATOWN ) @@ -104,7 +104,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : x.selectFirst("b")?.ownText() == "Genre(s):" }?.select("a")?.mapNotNull { a -> MangaTag( - title = a.attr("title"), + title = a.attr("title").toTitleCase(), key = a.attr("href").parseTagKey() ?: return@mapNotNull null, source = MangaSource.MANGATOWN ) @@ -172,7 +172,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : MangaTag( source = MangaSource.MANGATOWN, key = key, - title = a.text() + title = a.text().toTitleCase() ) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt index 2b2f9b8cd..6aa94cb98 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt @@ -62,7 +62,7 @@ class MangareadRepository( tags = summary?.selectFirst(".mg_genres")?.select("a")?.mapToSet { a -> MangaTag( key = a.attr("href").removeSuffix("/").substringAfterLast('/'), - title = a.text(), + title = a.text().toTitleCase(), source = MangaSource.MANGAREAD ) }.orEmpty(), @@ -91,7 +91,7 @@ class MangareadRepository( } MangaTag( key = href, - title = a.text(), + title = a.text().toTitleCase(), source = MangaSource.MANGAREAD ) } @@ -113,7 +113,7 @@ class MangareadRepository( ?.mapNotNullToSet { a -> MangaTag( key = a.attr("href").removeSuffix("/").substringAfterLast('/'), - title = a.text(), + title = a.text().toTitleCase(), source = MangaSource.MANGAREAD ) } ?: manga.tags, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt index 7b782ab1c..351467882 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt @@ -94,7 +94,7 @@ abstract class NineMangaRepository( tags = infoRoot.getElementsByAttributeValue("itemprop", "genre").first() ?.select("a")?.mapToSet { a -> MangaTag( - title = a.text(), + title = a.text().toTitleCase(), key = a.attr("href").substringBetween("/", "."), source = source, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt index ebea9bc94..d3925d62a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt @@ -73,7 +73,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito author = null, tags = jo.optJSONArray("genres")?.mapToSet { g -> MangaTag( - title = g.getString("name"), + title = g.getString("name").toTitleCase(), key = g.getInt("id").toString(), source = MangaSource.REMANGA ) @@ -109,7 +109,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito }, tags = content.getJSONArray("genres").mapToSet { g -> MangaTag( - title = g.getString("name"), + title = g.getString("name").toTitleCase(), key = g.getInt("id").toString(), source = MangaSource.REMANGA ) @@ -175,7 +175,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito .parseJson().getJSONObject("content").getJSONArray("genres") return content.mapToSet { jo -> MangaTag( - title = jo.getString("name"), + title = jo.getString("name").toTitleCase(), key = jo.getInt("id").toString(), source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt index f678b83b7..791e9985a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt @@ -7,10 +7,7 @@ import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaTag -import org.koitharu.kotatsu.utils.ext.getBooleanOrDefault -import org.koitharu.kotatsu.utils.ext.getLongOrDefault -import org.koitharu.kotatsu.utils.ext.getStringOrNull -import org.koitharu.kotatsu.utils.ext.mapToSet +import org.koitharu.kotatsu.utils.ext.* class MangaIndex(source: String?) { @@ -61,7 +58,7 @@ class MangaIndex(source: String?) { description = json.getStringOrNull("description"), tags = json.getJSONArray("tags").mapToSet { x -> MangaTag( - title = x.getString("title"), + title = x.getString("title").toTitleCase(), key = x.getString("key"), source = source ) From a9454a1455e8599a6e691210412e6d2398dcb550 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 2 Mar 2022 18:11:56 +0200 Subject: [PATCH 05/23] Suggestions update action --- .../reader/ui/SimpleSettingsActivity.kt | 15 +++++--- .../suggestions/ui/SuggestionsFragment.kt | 35 +++++++++++++++++-- .../suggestions/ui/SuggestionsViewModel.kt | 12 ++++--- .../kotatsu/tracker/ui/FeedFragment.kt | 2 +- app/src/main/res/menu/opt_suggestions.xml | 18 ++++++++++ 5 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/menu/opt_suggestions.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt index 8efab6827..c32605b96 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt @@ -14,10 +14,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.databinding.ActivitySettingsSimpleBinding -import org.koitharu.kotatsu.settings.MainSettingsFragment -import org.koitharu.kotatsu.settings.NetworkSettingsFragment -import org.koitharu.kotatsu.settings.ReaderSettingsFragment -import org.koitharu.kotatsu.settings.SourceSettingsFragment +import org.koitharu.kotatsu.settings.* class SimpleSettingsActivity : BaseActivity() { @@ -27,9 +24,11 @@ class SimpleSettingsActivity : BaseActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) supportFragmentManager.commit { replace( - R.id.container, when (intent?.action) { + R.id.container, + when (intent?.action) { Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment() ACTION_READER -> ReaderSettingsFragment() + ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() ACTION_SOURCE -> SourceSettingsFragment.newInstance( intent.getParcelableExtra(EXTRA_SOURCE) ?: MangaSource.LOCAL ) @@ -55,6 +54,8 @@ class SimpleSettingsActivity : BaseActivity() { private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS" + private const val ACTION_SUGGESTIONS = + "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS" private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS" private const val EXTRA_SOURCE = "source" @@ -63,6 +64,10 @@ class SimpleSettingsActivity : BaseActivity() { Intent(context, SimpleSettingsActivity::class.java) .setAction(ACTION_READER) + fun newSuggestionsSettingsIntent(context: Context) = + Intent(context, SimpleSettingsActivity::class.java) + .setAction(ACTION_SUGGESTIONS) + fun newSourceSettingsIntent(context: Context, source: MangaSource) = Intent(context, SimpleSettingsActivity::class.java) .setAction(ACTION_SOURCE) diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt index b4f61427b..c70793405 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -1,18 +1,47 @@ package org.koitharu.kotatsu.suggestions.ui import android.os.Bundle -import android.view.View +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.list.ui.MangaListFragment +import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity class SuggestionsFragment : MangaListFragment() { override val viewModel by viewModel() override val isSwipeRefreshEnabled = false - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.opt_suggestions, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_update -> { + SuggestionsWorker.startNow(requireContext()) + Snackbar.make( + binding.recyclerView, + R.string.feed_will_update_soon, + Snackbar.LENGTH_LONG, + ).show() + true + } + R.id.action_settings -> { + startActivity(SimpleSettingsActivity.newSuggestionsSettingsIntent(requireContext())) + true + } + else -> super.onOptionsItemSelected(item) + } } override fun onScrolledToEnd() = Unit diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt index a15099d68..9d5190a93 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt @@ -7,10 +7,7 @@ import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaListViewModel -import org.koitharu.kotatsu.list.ui.model.EmptyState -import org.koitharu.kotatsu.list.ui.model.LoadingState -import org.koitharu.kotatsu.list.ui.model.toErrorState -import org.koitharu.kotatsu.list.ui.model.toUi +import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.onFirst @@ -20,6 +17,8 @@ class SuggestionsViewModel( settings: AppSettings, ) : MangaListViewModel(settings) { + private val headerModel = ListHeader(null, R.string.suggestions) + override val content = combine( repository.observeAll(), createListModeFlow() @@ -30,7 +29,10 @@ class SuggestionsViewModel( textPrimary = R.string.nothing_found, textSecondary = R.string.text_suggestion_holder, )) - else -> list.toUi(mode) + else -> buildList(list.size + 1) { + add(headerModel) + list.toUi(this, mode) + } } }.onFirst { isLoading.postValue(false) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt index a98bf12ad..2fd2448f2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt @@ -78,7 +78,7 @@ class FeedFragment : BaseFragment(), PaginationScrollListen Snackbar.make( binding.recyclerView, R.string.feed_will_update_soon, - Snackbar.LENGTH_SHORT + Snackbar.LENGTH_LONG, ).show() true } diff --git a/app/src/main/res/menu/opt_suggestions.xml b/app/src/main/res/menu/opt_suggestions.xml new file mode 100644 index 000000000..5e665f49e --- /dev/null +++ b/app/src/main/res/menu/opt_suggestions.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file From 238bc89be9bcae537630d4bfcd529014d56132fc Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 2 Mar 2022 21:05:57 +0200 Subject: [PATCH 06/23] Move filter into bottom sheet --- .../koitharu/kotatsu/base/ui/BaseViewModel.kt | 2 +- .../kotatsu/core/model/MangaFilter.kt | 10 --- .../kotatsu/list/ui/MangaListFragment.kt | 47 ---------- .../kotatsu/list/ui/MangaListViewModel.kt | 71 +-------------- .../{FilterAdapter2.kt => FilterAdapter.kt} | 3 +- .../list/ui/filter/FilterAdapterDelegates.kt | 7 +- .../list/ui/filter/FilterBottomSheet.kt | 81 +++++++++++++++++ .../list/ui/filter/FilterDiffCallback.kt | 2 + .../kotatsu/list/ui/filter/FilterItem.kt | 2 + .../kotatsu/list/ui/filter/FilterState.kt | 12 +++ .../kotatsu/list/ui/filter/FilterViewModel.kt | 89 +++++++++++++++++++ .../kotatsu/remotelist/RemoteListModule.kt | 9 +- .../remotelist/ui/RemoteListFragment.kt | 23 ++++- .../remotelist/ui/RemoteListViewModel.kt | 49 +++++----- .../main/res/layout-w600dp/fragment_list.xml | 50 ----------- app/src/main/res/layout/fragment_list.xml | 34 ++----- .../res/layout/item_checkable_multiple.xml | 2 +- .../main/res/layout/item_checkable_single.xml | 2 +- app/src/main/res/layout/sheet_filter.xml | 33 +++++++ app/src/main/res/menu/opt_list.xml | 5 -- app/src/main/res/menu/opt_list_remote.xml | 6 ++ 21 files changed, 298 insertions(+), 241 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt rename app/src/main/java/org/koitharu/kotatsu/list/ui/filter/{FilterAdapter2.kt => FilterAdapter.kt} (86%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt delete mode 100644 app/src/main/res/layout-w600dp/fragment_list.xml create mode 100644 app/src/main/res/layout/sheet_filter.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt index b3df5277a..8d1c5e279 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt @@ -33,7 +33,7 @@ abstract class BaseViewModel : ViewModel() { } } - private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> + protected fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> if (BuildConfig.DEBUG) { throwable.printStackTrace() } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt deleted file mode 100644 index 498492f24..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.koitharu.kotatsu.core.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class MangaFilter( - val sortOrder: SortOrder?, - val tags: Set, -) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index a91d3924b..01f40436b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -4,13 +4,9 @@ import android.os.Bundle import android.view.* import androidx.annotation.CallSuper import androidx.appcompat.widget.PopupMenu -import androidx.core.content.ContextCompat import androidx.core.graphics.Insets import androidx.core.view.GravityCompat -import androidx.core.view.isGone -import androidx.core.view.isVisible import androidx.core.view.updatePadding -import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout @@ -30,8 +26,6 @@ import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter -import org.koitharu.kotatsu.list.ui.filter.FilterAdapter2 -import org.koitharu.kotatsu.list.ui.filter.FilterItem import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.main.ui.MainActivity @@ -43,7 +37,6 @@ abstract class MangaListFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener { private var listAdapter: MangaListAdapter? = null - private var filterAdapter: FilterAdapter2? = null private var paginationListener: PaginationScrollListener? = null private val spanResolver = MangaListSpanResolver() private val spanSizeLookup = SpanSizeLookup() @@ -51,7 +44,6 @@ abstract class MangaListFragment : BaseFragment(), spanSizeLookup.invalidateCache() } open val isSwipeRefreshEnabled = true - private var drawer: DrawerLayout? = null protected abstract val viewModel: MangaListViewModel @@ -67,8 +59,6 @@ abstract class MangaListFragment : BaseFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - drawer = binding.root as? DrawerLayout - drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) listAdapter = MangaListAdapter( coil = get(), lifecycleOwner = viewLifecycleOwner, @@ -76,7 +66,6 @@ abstract class MangaListFragment : BaseFragment(), onRetryClick = ::resolveException, onTagRemoveClick = viewModel::onRemoveFilterTag ) - filterAdapter = FilterAdapter2(viewModel) paginationListener = PaginationScrollListener(4, this) with(binding.recyclerView) { setHasFixedSize(true) @@ -89,17 +78,12 @@ abstract class MangaListFragment : BaseFragment(), setOnRefreshListener(this@MangaListFragment) isEnabled = isSwipeRefreshEnabled } - with(binding.recyclerViewFilter) { - setHasFixedSize(true) - adapter = filterAdapter - } (parentFragment as? RecycledViewPoolHolder)?.let { binding.recyclerView.setRecycledViewPool(it.recycledViewPool) } viewModel.content.observe(viewLifecycleOwner, ::onListChanged) - viewModel.filter.observe(viewLifecycleOwner, ::onInitFilter) viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged) @@ -107,9 +91,7 @@ abstract class MangaListFragment : BaseFragment(), } override fun onDestroyView() { - drawer = null listAdapter = null - filterAdapter = null paginationListener = null spanSizeLookup.invalidateCache() super.onDestroyView() @@ -125,19 +107,9 @@ abstract class MangaListFragment : BaseFragment(), ListModeSelectDialog.show(childFragmentManager) true } - R.id.action_filter -> { - drawer?.toggleDrawer(GravityCompat.END) - true - } else -> super.onOptionsItemSelected(item) } - override fun onPrepareOptionsMenu(menu: Menu) { - menu.findItem(R.id.action_filter).isVisible = drawer != null && - drawer?.getDrawerLockMode(GravityCompat.END) != DrawerLayout.LOCK_MODE_LOCKED_CLOSED - super.onPrepareOptionsMenu(menu) - } - override fun onItemClick(item: Manga, view: View) { startActivity(DetailsActivity.newIntent(context ?: return, item)) } @@ -200,27 +172,8 @@ abstract class MangaListFragment : BaseFragment(), } } - protected fun onInitFilter(filter: List) { - filterAdapter?.items = filter - drawer?.setDrawerLockMode( - if (filter.isEmpty()) { - DrawerLayout.LOCK_MODE_LOCKED_CLOSED - } else { - DrawerLayout.LOCK_MODE_UNLOCKED - } - ) ?: binding.dividerFilter?.let { - it.isGone = filter.isEmpty() - binding.recyclerViewFilter.isVisible = it.isVisible - } - activity?.invalidateOptionsMenu() - } - override fun onWindowInsetsChanged(insets: Insets) { val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top - binding.recyclerViewFilter.updatePadding( - top = headerHeight, - bottom = insets.bottom - ) binding.root.updatePadding( left = insets.left, right = insets.right diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index e2a463f4e..6a04449b6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -1,32 +1,22 @@ package org.koitharu.kotatsu.list.ui -import androidx.annotation.CallSuper import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.* -import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode -import org.koitharu.kotatsu.list.domain.AvailableFilters -import org.koitharu.kotatsu.list.ui.filter.FilterItem -import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct abstract class MangaListViewModel( private val settings: AppSettings, -) : BaseViewModel(), OnFilterChangedListener { +) : BaseViewModel() { abstract val content: LiveData> - val filter = MutableLiveData>() val listMode = MutableLiveData() val gridScale = settings.observe() .filter { it == AppSettings.KEY_GRID_SIZE } @@ -35,6 +25,8 @@ abstract class MangaListViewModel( settings.gridSize / 100f } + open fun onRemoveFilterTag(tag: MangaTag) = Unit + protected fun createListModeFlow() = settings.observe() .filter { it == AppSettings.KEY_LIST_MODE } .map { settings.listMode } @@ -46,63 +38,6 @@ abstract class MangaListViewModel( } } - protected var currentFilter: MangaFilter = MangaFilter(null, emptySet()) - private set(value) { - field = value - onFilterChanged() - } - protected var availableFilters: AvailableFilters? = null - private var filterJob: Job? = null - - final override fun onSortItemClick(item: FilterItem.Sort) { - currentFilter = currentFilter.copy(sortOrder = item.order) - } - - final override fun onTagItemClick(item: FilterItem.Tag) { - val tags = if (item.isChecked) { - currentFilter.tags - item.tag - } else { - currentFilter.tags + item.tag - } - currentFilter = currentFilter.copy(tags = tags) - } - - fun onRemoveFilterTag(tag: MangaTag) { - val tags = currentFilter.tags - if (tag !in tags) { - return - } - currentFilter = currentFilter.copy(tags = tags - tag) - } - - @CallSuper - open fun onFilterChanged() { - val previousJob = filterJob - filterJob = launchJob(Dispatchers.Default) { - previousJob?.cancelAndJoin() - filter.postValue( - availableFilters?.run { - val list = ArrayList(size + 2) - if (sortOrders.isNotEmpty()) { - val selectedSort = currentFilter.sortOrder ?: sortOrders.first() - list += FilterItem.Header(R.string.sort_order) - sortOrders.sortedBy { it.ordinal }.mapTo(list) { - FilterItem.Sort(it, isSelected = it == selectedSort) - } - } - if (tags.isNotEmpty()) { - list += FilterItem.Header(R.string.genres) - tags.sortedBy { it.title }.mapTo(list) { - FilterItem.Tag(it, isChecked = it in currentFilter.tags) - } - } - ensureActive() - list - }.orEmpty() - ) - } - } - abstract fun onRefresh() abstract fun onRetry() diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt rename to app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index 67b4d3585..3af68c0f4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -2,11 +2,12 @@ package org.koitharu.kotatsu.list.ui.filter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -class FilterAdapter2( +class FilterAdapter( listener: OnFilterChangedListener, ) : AsyncListDifferDelegationAdapter( FilterDiffCallback(), filterSortDelegate(listener), filterTagDelegate(listener), filterHeaderDelegate(), + filterLoadingDelegate(), ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt index 8b926d768..10a6cafd8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt @@ -4,6 +4,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding +import org.koitharu.kotatsu.databinding.ItemLoadingFooterBinding fun filterSortDelegate( listener: OnFilterChangedListener, @@ -44,4 +45,8 @@ fun filterHeaderDelegate() = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemLoadingFooterBinding.inflate(layoutInflater, parent, false) } +) { } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt new file mode 100644 index 000000000..c47d7ac00 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt @@ -0,0 +1,81 @@ +package org.koitharu.kotatsu.list.ui.filter + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.FragmentManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseBottomSheet +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.databinding.SheetFilterBinding +import org.koitharu.kotatsu.utils.ext.withArgs + +class FilterBottomSheet : BaseBottomSheet() { + + private val viewModel by viewModel { + parametersOf( + requireArguments().getParcelable(ARG_SOURCE), + requireArguments().getParcelable(ARG_STATE), + ) + } + + override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { + return SheetFilterBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.toolbar.setNavigationOnClickListener { dismiss() } + if (!resources.getBoolean(R.bool.is_tablet)) { + binding.toolbar.navigationIcon = null + } + val adapter = FilterAdapter(viewModel) + binding.recyclerView.adapter = adapter + viewModel.filter.observe(viewLifecycleOwner, adapter::setItems) + viewModel.result.observe(viewLifecycleOwner) { + parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(ARG_STATE to it)) + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).also { + val behavior = (it as? BottomSheetDialog)?.behavior ?: return@also + behavior.addBottomSheetCallback( + object : BottomSheetBehavior.BottomSheetCallback() { + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit + + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_EXPANDED) { + binding.toolbar.setNavigationIcon(R.drawable.ic_cross) + } else { + binding.toolbar.navigationIcon = null + } + } + } + ) + } + + companion object { + + const val REQUEST_KEY = "filter" + + const val ARG_STATE = "state" + private const val TAG = "FilterBottomSheet" + private const val ARG_SOURCE = "source" + + fun show( + fm: FragmentManager, + source: MangaSource, + state: FilterState, + ) = FilterBottomSheet().withArgs(2) { + putParcelable(ARG_SOURCE, source) + putParcelable(ARG_STATE, state) + }.show(fm, TAG) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt index 1ccd4e813..d72cadf7c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt @@ -6,6 +6,7 @@ class FilterDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { return when { + oldItem === newItem -> true oldItem.javaClass != newItem.javaClass -> false oldItem is FilterItem.Header && newItem is FilterItem.Header -> { oldItem.titleResId == newItem.titleResId @@ -22,6 +23,7 @@ class FilterDiffCallback : DiffUtil.ItemCallback() { override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { return when { + oldItem === newItem -> true oldItem is FilterItem.Header && newItem is FilterItem.Header -> true oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { oldItem.isChecked == newItem.isChecked diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt index a74d93b1d..8117f5afd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt @@ -19,4 +19,6 @@ sealed interface FilterItem { val tag: MangaTag, val isChecked: Boolean, ) : FilterItem + + object Loading : FilterItem } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt new file mode 100644 index 000000000..1c1c8a9cf --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.list.ui.filter + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.koitharu.kotatsu.core.model.MangaTag +import org.koitharu.kotatsu.core.model.SortOrder + +@Parcelize +class FilterState( + val sortOrder: SortOrder?, + val tags: Set, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt new file mode 100644 index 000000000..7341e836a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt @@ -0,0 +1,89 @@ +package org.koitharu.kotatsu.list.ui.filter + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.* +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.SortOrder +import org.koitharu.kotatsu.core.parser.MangaRepository +import java.util.* + +class FilterViewModel( + private val repository: MangaRepository, + state: FilterState, +) : BaseViewModel(), OnFilterChangedListener { + + val filter = MutableLiveData>() + val result = MutableLiveData() + private var job: Job? = null + private var selectedSortOrder: SortOrder? = state.sortOrder + private val selectedTags = HashSet(state.tags) + private val availableTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { + repository.getTags() + } + + init { + showFilter() + } + + override fun onSortItemClick(item: FilterItem.Sort) { + selectedSortOrder = item.order + updateFilters() + } + + override fun onTagItemClick(item: FilterItem.Tag) { + val isModified = if (item.isChecked) { + selectedTags.remove(item.tag) + } else { + selectedTags.add(item.tag) + } + if (isModified) { + updateFilters() + } + } + + private fun updateFilters() { + val previousJob = job + job = launchJob(Dispatchers.Default) { + previousJob?.cancelAndJoin() + val tags = availableTagsDeferred.await() + val sortOrders = repository.sortOrders + val list = ArrayList(sortOrders.size + tags.size + 2) + list.add(FilterItem.Header(R.string.sort_order)) + sortOrders.sortedBy { it.ordinal }.mapTo(list) { + FilterItem.Sort(it, isSelected = it == selectedSortOrder) + } + if (tags.isNotEmpty() || selectedTags.isNotEmpty()) { + list.add(FilterItem.Header(R.string.genres)) + val mappedTags = TreeSet(compareBy({ !it.isChecked }, { it.tag.title })) + tags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } + selectedTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = true) } + list.addAll(mappedTags) + } + ensureActive() + filter.postValue(list) + } + result.value = FilterState(selectedSortOrder, selectedTags) + } + + private fun showFilter() { + job = launchJob(Dispatchers.Default) { + val sortOrders = repository.sortOrders + val list = ArrayList(sortOrders.size + selectedTags.size + 3) + list.add(FilterItem.Header(R.string.sort_order)) + sortOrders.sortedBy { it.ordinal }.mapTo(list) { + FilterItem.Sort(it, isSelected = it == selectedSortOrder) + } + if (selectedTags.isNotEmpty()) { + list.add(FilterItem.Header(R.string.genres)) + selectedTags.sortedBy { it.title }.mapTo(list) { + FilterItem.Tag(it, isChecked = it in selectedTags) + } + } + list.add(FilterItem.Loading) + filter.postValue(list) + updateFilters() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt index 4555fe10e..b856248ba 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt @@ -4,12 +4,17 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.list.ui.filter.FilterViewModel import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel val remoteListModule get() = module { - viewModel { source -> - RemoteListViewModel(get(named(source.get())), get()) + viewModel { params -> + RemoteListViewModel(get(named(params.get())), get()) + } + + viewModel { params -> + FilterViewModel(get(named(params.get())), params.get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 5ae3a92da..ff951de7b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -1,18 +1,22 @@ package org.koitharu.kotatsu.remotelist.ui +import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import android.view.View +import androidx.fragment.app.FragmentResultListener import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.list.ui.MangaListFragment +import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity import org.koitharu.kotatsu.utils.ext.parcelableArgument import org.koitharu.kotatsu.utils.ext.withArgs -class RemoteListFragment : MangaListFragment() { +class RemoteListFragment : MangaListFragment(), FragmentResultListener { override val viewModel by viewModel { parametersOf(source) @@ -20,6 +24,11 @@ class RemoteListFragment : MangaListFragment() { private val source by parcelableArgument(ARG_SOURCE) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + childFragmentManager.setFragmentResultListener(FilterBottomSheet.REQUEST_KEY, viewLifecycleOwner, this) + } + override fun onScrolledToEnd() { viewModel.loadNextPage() } @@ -44,10 +53,22 @@ class RemoteListFragment : MangaListFragment() { ) true } + R.id.action_filter -> { + FilterBottomSheet.show(childFragmentManager, source, viewModel.filter) + true + } else -> super.onOptionsItemSelected(item) } } + override fun onFragmentResult(requestKey: String, result: Bundle) { + when (requestKey) { + FilterBottomSheet.REQUEST_KEY -> viewModel.applyFilter( + result.getParcelable(FilterBottomSheet.ARG_STATE) ?: return + ) + } + } + companion object { private const val ARG_SOURCE = "provider" diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index e92616d4c..fc5acd438 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -9,11 +9,12 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.list.domain.AvailableFilters import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.list.ui.filter.FilterState import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct @@ -22,6 +23,8 @@ class RemoteListViewModel( settings: AppSettings ) : MangaListViewModel(settings) { + var filter = FilterState(repository.sortOrders.firstOrNull(), emptySet()) + private set private val mangaList = MutableStateFlow?>(null) private val hasNextPage = MutableStateFlow(false) private val listError = MutableStateFlow(null) @@ -54,7 +57,6 @@ class RemoteListViewModel( init { loadList(false) - loadFilter() } override fun onRefresh() { @@ -65,12 +67,27 @@ class RemoteListViewModel( loadList(append = !mangaList.value.isNullOrEmpty()) } + override fun onRemoveFilterTag(tag: MangaTag) { + val tags = filter.tags + if (tag !in tags) { + return + } + applyFilter(FilterState(filter.sortOrder, tags - tag)) + } + fun loadNextPage() { if (hasNextPage.value && listError.value == null) { loadList(append = true) } } + fun applyFilter(newFilter: FilterState) { + filter = newFilter + mangaList.value = null + hasNextPage.value = false + loadList(false) + } + private fun loadList(append: Boolean) { if (loadingJob?.isActive == true) { return @@ -80,8 +97,8 @@ class RemoteListViewModel( listError.value = null val list = repository.getList2( offset = if (append) mangaList.value?.size ?: 0 else 0, - sortOrder = currentFilter.sortOrder, - tags = currentFilter.tags, + sortOrder = filter.sortOrder, + tags = filter.tags, ) if (!append) { mangaList.value = list @@ -98,34 +115,12 @@ class RemoteListViewModel( } } - override fun onFilterChanged() { - super.onFilterChanged() - mangaList.value = null - hasNextPage.value = false - loadList(false) - } - private fun createFilterModel(): CurrentFilterModel? { - val tags = currentFilter.tags + val tags = filter.tags return if (tags.isEmpty()) { null } else { CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) }) } } - - private fun loadFilter() { - launchJob(Dispatchers.Default) { - try { - val sorts = repository.sortOrders - val tags = repository.getTags() - availableFilters = AvailableFilters(sorts, tags) - onFilterChanged() - } catch (e: Exception) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } - } - } - } } diff --git a/app/src/main/res/layout-w600dp/fragment_list.xml b/app/src/main/res/layout-w600dp/fragment_list.xml deleted file mode 100644 index c3041768c..000000000 --- a/app/src/main/res/layout-w600dp/fragment_list.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_list.xml b/app/src/main/res/layout/fragment_list.xml index 93abec4b7..a4e4cb419 100644 --- a/app/src/main/res/layout/fragment_list.xml +++ b/app/src/main/res/layout/fragment_list.xml @@ -1,39 +1,21 @@ - - - - - - - + tools:listitem="@layout/item_manga_list" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_checkable_multiple.xml b/app/src/main/res/layout/item_checkable_multiple.xml index 7871b30a6..2feb5f5aa 100644 --- a/app/src/main/res/layout/item_checkable_multiple.xml +++ b/app/src/main/res/layout/item_checkable_multiple.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="?android:listPreferredItemHeightSmall" - android:background="?android:selectableItemBackground" + android:background="?selectableItemBackground" android:drawableStart="?android:listChoiceIndicatorMultiple" android:drawablePadding="12dp" android:gravity="center_vertical|start" diff --git a/app/src/main/res/layout/item_checkable_single.xml b/app/src/main/res/layout/item_checkable_single.xml index a9c19ed8a..cec15830e 100644 --- a/app/src/main/res/layout/item_checkable_single.xml +++ b/app/src/main/res/layout/item_checkable_single.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="?android:listPreferredItemHeightSmall" - android:background="?android:selectableItemBackground" + android:background="?selectableItemBackground" android:drawableStart="?android:listChoiceIndicatorSingle" android:drawablePadding="12dp" android:gravity="center_vertical|start" diff --git a/app/src/main/res/layout/sheet_filter.xml b/app/src/main/res/layout/sheet_filter.xml new file mode 100644 index 000000000..b7343028b --- /dev/null +++ b/app/src/main/res/layout/sheet_filter.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_list.xml b/app/src/main/res/menu/opt_list.xml index 09ef204b0..421f8a2c5 100644 --- a/app/src/main/res/menu/opt_list.xml +++ b/app/src/main/res/menu/opt_list.xml @@ -9,9 +9,4 @@ android:title="@string/list_mode" app:showAsAction="never" /> - \ No newline at end of file diff --git a/app/src/main/res/menu/opt_list_remote.xml b/app/src/main/res/menu/opt_list_remote.xml index deb531840..5df3276f1 100644 --- a/app/src/main/res/menu/opt_list_remote.xml +++ b/app/src/main/res/menu/opt_list_remote.xml @@ -3,6 +3,12 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + + Date: Thu, 3 Mar 2022 18:31:47 +0200 Subject: [PATCH 07/23] Open filter from list header --- .../base/domain/MangaDataRepository.kt | 9 ++++ .../base/ui/widgets/AnimatedToolbar.kt | 41 ------------------- .../koitharu/kotatsu/core/db/dao/TagsDao.kt | 4 +- .../core/parser/RemoteMangaRepository.kt | 3 -- .../core/parser/site/ExHentaiRepository.kt | 2 + .../history/ui/HistoryListViewModel.kt | 2 +- .../kotatsu/list/ui/MangaListFragment.kt | 5 ++- .../kotatsu/list/ui/adapter/ListHeaderAD.kt | 28 ++++++++++++- .../list/ui/adapter/MangaListAdapter.kt | 3 ++ .../list/ui/filter/FilterBottomSheet.kt | 7 +++- .../kotatsu/list/ui/filter/FilterViewModel.kt | 11 ++++- .../kotatsu/list/ui/model/ListHeader.kt | 2 + .../kotatsu/local/ui/LocalListViewModel.kt | 2 +- .../kotatsu/remotelist/RemoteListModule.kt | 13 +++++- .../remotelist/ui/RemoteListFragment.kt | 6 ++- .../remotelist/ui/RemoteListViewModel.kt | 13 +++--- .../res/layout/item_header_with_filter.xml | 36 ++++++++++++++++ 17 files changed, 125 insertions(+), 62 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt create mode 100644 app/src/main/res/layout/item_header_with_filter.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt index 06f22090a..e51857f55 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt @@ -6,7 +6,10 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.prefs.ReaderMode +import org.koitharu.kotatsu.utils.ext.mapToSet class MangaDataRepository(private val db: MangaDatabase) { @@ -45,4 +48,10 @@ class MangaDataRepository(private val db: MangaDatabase) { db.mangaDao.upsert(MangaEntity.from(manga), tags) } } + + suspend fun findTags(source: MangaSource): Set { + return db.tagsDao.findTags(source.name).mapToSet { + it.toMangaTag() + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt deleted file mode 100644 index f17b31f84..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.koitharu.kotatsu.base.ui.widgets - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.View -import androidx.appcompat.widget.Toolbar -import androidx.core.view.isGone -import com.google.android.material.R -import com.google.android.material.appbar.MaterialToolbar -import java.lang.reflect.Field - -class AnimatedToolbar @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.toolbarStyle, -) : MaterialToolbar(context, attrs, defStyleAttr) { - - private var navButtonView: View? = null - get() { - if (field == null) { - runCatching { - field = navButtonViewField?.get(this) as? View - } - } - return field - } - - override fun setNavigationIcon(icon: Drawable?) { - super.setNavigationIcon(icon) - navButtonView?.isGone = (icon == null) - } - - private companion object { - - val navButtonViewField: Field? = runCatching { - Toolbar::class.java.getDeclaredField("mNavButtonView") - .also { it.isAccessible = true } - }.getOrNull() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt index 0cd94ba37..7f9655d19 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt @@ -6,8 +6,8 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity @Dao abstract class TagsDao { - @Query("SELECT * FROM tags") - abstract suspend fun getAllTags(): List + @Query("SELECT * FROM tags WHERE source = :source") + abstract suspend fun findTags(source: String): List @Insert(onConflict = OnConflictStrategy.IGNORE) abstract suspend fun insert(tag: TagEntity): Long diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 4a2c2be82..f66e4b14f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -4,7 +4,6 @@ import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaTag -import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.prefs.SourceSettings abstract class RemoteMangaRepository( @@ -20,8 +19,6 @@ abstract class RemoteMangaRepository( val title: String get() = source.title - override val sortOrders: Set get() = emptySet() - override suspend fun getPageUrl(page: MangaPage): String = page.url.withDomain() override suspend fun getTags(): Set = emptySet() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt index 41b86750e..0077f3d8c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt @@ -17,6 +17,8 @@ class ExHentaiRepository( override val source = MangaSource.EXHENTAI + override val sortOrders: Set = emptySet() + override val defaultDomain: String get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 97fa18c4c..97664dd2f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -85,7 +85,7 @@ class HistoryListViewModel( val result = ArrayList(if (grouped) (list.size * 1.4).toInt() else list.size + 1) var prevDate: DateTimeAgo? = null if (!grouped) { - result += ListHeader(null, R.string.history) + result += ListHeader(null, R.string.history, null) } for ((manga, history) in list) { if (grouped) { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 01f40436b..00bd769fd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -64,7 +64,8 @@ abstract class MangaListFragment : BaseFragment(), lifecycleOwner = viewLifecycleOwner, clickListener = this, onRetryClick = ::resolveException, - onTagRemoveClick = viewModel::onRemoveFilterTag + onTagRemoveClick = viewModel::onRemoveFilterTag, + onFilterClickListener = this::onFilterClick, ) paginationListener = PaginationScrollListener(4, this) with(binding.recyclerView) { @@ -191,6 +192,8 @@ abstract class MangaListFragment : BaseFragment(), } } + protected open fun onFilterClick() = Unit + private fun onGridScaleChanged(scale: Float) { spanSizeLookup.invalidateCache() spanResolver.setGridSize(scale, binding.recyclerView) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt index 4d25060ac..53ac01484 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt @@ -2,11 +2,16 @@ package org.koitharu.kotatsu.list.ui.adapter import android.widget.TextView import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemHeaderWithFilterBinding import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel -fun listHeaderAD() = adapterDelegate(R.layout.item_header) { +fun listHeaderAD() = adapterDelegate( + layout = R.layout.item_header, + on = { item, _, _ -> item is ListHeader && item.sortOrder == null }, +) { bind { val textView = (itemView as TextView) @@ -16,4 +21,25 @@ fun listHeaderAD() = adapterDelegate(R.layout.item_header textView.setText(item.textRes) } } +} + +fun listHeaderWithFilterAD( + onFilterClickListener: () -> Unit, +) = adapterDelegateViewBinding( + viewBinding = { inflater, parent -> ItemHeaderWithFilterBinding.inflate(inflater, parent, false) }, + on = { item, _, _ -> item is ListHeader && item.sortOrder != null }, +) { + + binding.textViewFilter.setOnClickListener { + onFilterClickListener() + } + + bind { + if (item.text != null) { + binding.textViewTitle.text = item.text + } else { + binding.textViewTitle.setText(item.textRes) + } + binding.textViewFilter.setText(requireNotNull(item.sortOrder).titleRes) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 61cd60c03..714f04473 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -20,6 +20,7 @@ class MangaListAdapter( clickListener: OnListItemClickListener, onRetryClick: (Throwable) -> Unit, onTagRemoveClick: (MangaTag) -> Unit, + onFilterClickListener: () -> Unit, ) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { @@ -41,6 +42,7 @@ class MangaListAdapter( .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD()) .addDelegate(ITEM_TYPE_HEADER, listHeaderAD()) .addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick)) + .addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(onFilterClickListener)) } private class DiffCallback : DiffUtil.ItemCallback() { @@ -79,5 +81,6 @@ class MangaListAdapter( const val ITEM_TYPE_EMPTY = 8 const val ITEM_TYPE_HEADER = 9 const val ITEM_TYPE_FILTER = 10 + const val ITEM_TYPE_HEADER_FILTER = 11 } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt index c47d7ac00..70aef4326 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt @@ -8,7 +8,8 @@ import androidx.core.os.bundleOf import androidx.fragment.app.FragmentManager import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog -import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.androidx.viewmodel.ViewModelOwner.Companion.from +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseBottomSheet @@ -18,7 +19,9 @@ import org.koitharu.kotatsu.utils.ext.withArgs class FilterBottomSheet : BaseBottomSheet() { - private val viewModel by viewModel { + private val viewModel by sharedViewModel( + owner = { from(requireParentFragment(), requireParentFragment()) } + ) { parametersOf( requireArguments().getParcelable(ARG_SOURCE), requireArguments().getParcelable(ARG_STATE), diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt index 7341e836a..0942ef35d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt @@ -4,13 +4,15 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.* import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.model.SortOrder -import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import java.util.* class FilterViewModel( - private val repository: MangaRepository, + private val repository: RemoteMangaRepository, + dataRepository: MangaDataRepository, state: FilterState, ) : BaseViewModel(), OnFilterChangedListener { @@ -22,6 +24,9 @@ class FilterViewModel( private val availableTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { repository.getTags() } + private val localTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { + dataRepository.findTags(repository.source) + } init { showFilter() @@ -48,6 +53,7 @@ class FilterViewModel( job = launchJob(Dispatchers.Default) { previousJob?.cancelAndJoin() val tags = availableTagsDeferred.await() + val localTags = localTagsDeferred.await() val sortOrders = repository.sortOrders val list = ArrayList(sortOrders.size + tags.size + 2) list.add(FilterItem.Header(R.string.sort_order)) @@ -57,6 +63,7 @@ class FilterViewModel( if (tags.isNotEmpty() || selectedTags.isNotEmpty()) { list.add(FilterItem.Header(R.string.genres)) val mappedTags = TreeSet(compareBy({ !it.isChecked }, { it.tag.title })) + localTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } tags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } selectedTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = true) } list.addAll(mappedTags) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt index 209c7227f..a14db0f3a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt @@ -1,8 +1,10 @@ package org.koitharu.kotatsu.list.ui.model import androidx.annotation.StringRes +import org.koitharu.kotatsu.core.model.SortOrder data class ListHeader( val text: CharSequence?, @StringRes val textRes: Int, + val sortOrder: SortOrder?, ) : ListModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 3f721355a..b68af3a0a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -32,7 +32,7 @@ class LocalListViewModel( val importProgress = MutableLiveData(null) private val listError = MutableStateFlow(null) private val mangaList = MutableStateFlow?>(null) - private val headerModel = ListHeader(null, R.string.local_storage) + private val headerModel = ListHeader(null, R.string.local_storage, null) private var importJob: Job? = null override val content = combine( diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt index b856248ba..4d35a857f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt @@ -4,6 +4,8 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.list.ui.filter.FilterViewModel import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel @@ -11,10 +13,17 @@ val remoteListModule get() = module { viewModel { params -> - RemoteListViewModel(get(named(params.get())), get()) + RemoteListViewModel( + repository = get(named(params.get())) as RemoteMangaRepository, + settings = get(), + ) } viewModel { params -> - FilterViewModel(get(named(params.get())), params.get()) + FilterViewModel( + repository = get(named(params.get())) as RemoteMangaRepository, + dataRepository = get(), + state = params.get(), + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index ff951de7b..04a1ffefe 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -54,13 +54,17 @@ class RemoteListFragment : MangaListFragment(), FragmentResultListener { true } R.id.action_filter -> { - FilterBottomSheet.show(childFragmentManager, source, viewModel.filter) + onFilterClick() true } else -> super.onOptionsItemSelected(item) } } + override fun onFilterClick() { + FilterBottomSheet.show(childFragmentManager, source, viewModel.filter) + } + override fun onFragmentResult(requestKey: String, result: Bundle) { when (requestKey) { FilterBottomSheet.REQUEST_KEY -> viewModel.applyFilter( diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index fc5acd438..c914e709f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -10,7 +10,6 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaTag -import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaListViewModel @@ -19,7 +18,7 @@ import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct class RemoteListViewModel( - private val repository: MangaRepository, + private val repository: RemoteMangaRepository, settings: AppSettings ) : MangaListViewModel(settings) { @@ -29,21 +28,24 @@ class RemoteListViewModel( private val hasNextPage = MutableStateFlow(false) private val listError = MutableStateFlow(null) private var loadingJob: Job? = null - private val headerModel = ListHeader((repository as RemoteMangaRepository).title, 0) + private val headerModel = MutableStateFlow( + ListHeader(repository.title, 0, filter.sortOrder) + ) override val content = combine( mangaList, createListModeFlow(), + headerModel, listError, hasNextPage - ) { list, mode, error, hasNext -> + ) { list, mode, header, error, hasNext -> when { list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) list == null -> listOf(LoadingState) list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string.empty)) else -> { val result = ArrayList(list.size + 3) - result += headerModel + result += header createFilterModel()?.let { result.add(it) } list.toUi(result, mode) when { @@ -83,6 +85,7 @@ class RemoteListViewModel( fun applyFilter(newFilter: FilterState) { filter = newFilter + headerModel.value = ListHeader(repository.title, 0, newFilter.sortOrder) mangaList.value = null hasNextPage.value = false loadList(false) diff --git a/app/src/main/res/layout/item_header_with_filter.xml b/app/src/main/res/layout/item_header_with_filter.xml new file mode 100644 index 000000000..05c7793ba --- /dev/null +++ b/app/src/main/res/layout/item_header_with_filter.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file From 3c64d6675e86bdc90293adbe837fed6b02cb38f1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 4 Mar 2022 08:16:36 +0200 Subject: [PATCH 08/23] Handle filter loading errors --- .../kotatsu/list/ui/filter/FilterAdapter.kt | 1 + .../list/ui/filter/FilterAdapterDelegates.kt | 15 +++++--- .../list/ui/filter/FilterDiffCallback.kt | 6 +++- .../kotatsu/list/ui/filter/FilterItem.kt | 4 +++ .../kotatsu/list/ui/filter/FilterViewModel.kt | 34 ++++++++++++++----- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index 3af68c0f4..19b3f11f7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -10,4 +10,5 @@ class FilterAdapter( filterTagDelegate(listener), filterHeaderDelegate(), filterLoadingDelegate(), + filterErrorDelegate(), ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt index 10a6cafd8..073de2c9d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt @@ -1,10 +1,12 @@ package org.koitharu.kotatsu.list.ui.filter +import android.widget.TextView +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding -import org.koitharu.kotatsu.databinding.ItemLoadingFooterBinding fun filterSortDelegate( listener: OnFilterChangedListener, @@ -47,6 +49,11 @@ fun filterHeaderDelegate() = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemLoadingFooterBinding.inflate(layoutInflater, parent, false) } -) { } \ No newline at end of file +fun filterLoadingDelegate() = adapterDelegate(R.layout.item_loading_footer) {} + +fun filterErrorDelegate() = adapterDelegate(R.layout.item_sources_empty) { + + bind { + (itemView as TextView).setText(item.textResId) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt index d72cadf7c..73e3db315 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt @@ -17,14 +17,18 @@ class FilterDiffCallback : DiffUtil.ItemCallback() { oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { oldItem.order == newItem.order } + oldItem is FilterItem.Error && newItem is FilterItem.Error -> { + oldItem.textResId == newItem.textResId + } else -> false } } override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { return when { - oldItem === newItem -> true + oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true oldItem is FilterItem.Header && newItem is FilterItem.Header -> true + oldItem is FilterItem.Error && newItem is FilterItem.Error -> true oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { oldItem.isChecked == newItem.isChecked } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt index 8117f5afd..75b29e60d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt @@ -21,4 +21,8 @@ sealed interface FilterItem { ) : FilterItem object Loading : FilterItem + + class Error( + @StringRes val textResId: Int, + ) : FilterItem } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt index 0942ef35d..06c4b029e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.* import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import java.util.* @@ -21,12 +22,10 @@ class FilterViewModel( private var job: Job? = null private var selectedSortOrder: SortOrder? = state.sortOrder private val selectedTags = HashSet(state.tags) - private val availableTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { - repository.getTags() - } - private val localTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { + private val localTagsDeferred = viewModelScope.async(Dispatchers.Default) { dataRepository.findTags(repository.source) } + private var availableTagsDeferred = loadTagsAsync() init { showFilter() @@ -52,21 +51,24 @@ class FilterViewModel( val previousJob = job job = launchJob(Dispatchers.Default) { previousJob?.cancelAndJoin() - val tags = availableTagsDeferred.await() + val tags = tryLoadTags() val localTags = localTagsDeferred.await() val sortOrders = repository.sortOrders - val list = ArrayList(sortOrders.size + tags.size + 2) + val list = ArrayList(sortOrders.size + (tags?.size ?: 1) + 2) list.add(FilterItem.Header(R.string.sort_order)) sortOrders.sortedBy { it.ordinal }.mapTo(list) { FilterItem.Sort(it, isSelected = it == selectedSortOrder) } - if (tags.isNotEmpty() || selectedTags.isNotEmpty()) { + if (tags == null || tags.isNotEmpty() || selectedTags.isNotEmpty()) { list.add(FilterItem.Header(R.string.genres)) val mappedTags = TreeSet(compareBy({ !it.isChecked }, { it.tag.title })) localTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } - tags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } + tags?.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } selectedTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = true) } list.addAll(mappedTags) + if (tags == null) { + list.add(FilterItem.Error(R.string.filter_load_error)) + } } ensureActive() filter.postValue(list) @@ -93,4 +95,20 @@ class FilterViewModel( updateFilters() } } + + private suspend fun tryLoadTags(): Set? { + val shouldRetryOnError = availableTagsDeferred.isCompleted + val result = availableTagsDeferred.await() + if (result == null && shouldRetryOnError) { + availableTagsDeferred = loadTagsAsync() + return availableTagsDeferred.await() + } + return result + } + + private fun loadTagsAsync() = viewModelScope.async(Dispatchers.Default) { + kotlin.runCatching { + repository.getTags() + }.getOrNull() + } } \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3155e92f0..3966f6c81 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -253,4 +253,5 @@ Разрешить Запретить для NSFW Запретить всегда + Не удалось загрузить список жанров \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6adb549de..bd5d42803 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -255,4 +255,5 @@ Allow Block on NSFW Block always + Unable to load genres list \ No newline at end of file From 5c10dae028178b28a10463ed959fa929fdb0411e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 4 Mar 2022 13:58:27 +0100 Subject: [PATCH 09/23] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 98.8% (252 of 255 strings) Translated using Weblate (English) Currently translated at 100.0% (255 of 255 strings) Co-authored-by: Allan Nordhøy Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/ Translation: Kotatsu/Strings --- app/src/main/res/values-nb-rNO/strings.xml | 6 +++++- app/src/main/res/values/strings.xml | 12 ++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 26ee3bf54..c94015221 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -241,7 +241,7 @@ Innlogging på %s støttes ikke Du vil bli utlogget fra alle kilder Sjangere - Utelat NSFW-manga fra historikk + Utelat sensurerbar-manga fra historikk Datoformat Forvalg Du må angi ett navn @@ -252,4 +252,8 @@ Bruker et tema basert på fargene til bakgrunnen din Beregner … Importerer manga: %1$d av %2$d + Tillat + Blokker for sensurerbare + Alltid blokker + Skjermavbildningspraksis \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6adb549de..f47f1b04a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -180,7 +180,7 @@ Restore from backup Restored Preparing… - Create issue on GitHub + Create issue on GitHub File not found All data was restored The data was restored, but there are errors @@ -250,9 +250,9 @@ Available sources Dynamic theme Applies a theme created on the color scheme of your wallpaper - Importing manga: %1$d of %2$d - Screenshots policy - Allow - Block on NSFW - Block always + Importing manga: %1$d of %2$d + Screenshot policy + Allow + Block on NSFW + Always block \ No newline at end of file From 6d409168e308a251efd41c9b1744c7a571e524de Mon Sep 17 00:00:00 2001 From: Luiz-bro Date: Fri, 4 Mar 2022 13:58:27 +0100 Subject: [PATCH 10/23] Translated using Weblate (Portuguese) Currently translated at 100.0% (255 of 255 strings) Translated using Weblate (Portuguese) Currently translated at 99.2% (253 of 255 strings) Co-authored-by: Luiz-bro Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/ Translation: Kotatsu/Strings --- app/src/main/res/values-pt/strings.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index fc076e56d..7418212e2 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -202,7 +202,7 @@ O capítulo está em falta Autorizado O login em %s não é suportado - Géneros + Gêneros Tradução Você será desconectado de todas as fontes Vibração @@ -250,4 +250,10 @@ Padrão Tema dinâmico Aplica um tema criado no esquema de cores do seu papel de parede + Computando… + Importando mangá: %1$d de %2$d + Permitir + Bloquear no NSFW + Política de captura de tela + Sempre bloquear \ No newline at end of file From 6fa84066366c6017dbeb2c2cc666b17e8b5300c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Fri, 4 Mar 2022 13:58:28 +0100 Subject: [PATCH 11/23] Translated using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (255 of 255 strings) Co-authored-by: Oğuz Ersen Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/ Translation: Kotatsu/Strings --- app/src/main/res/values-tr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0982643a4..e16525c77 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -256,4 +256,5 @@ Uygunsuzlarda engelle Her zaman engelle İzin ver + Yeni bölümleri denetle \ No newline at end of file From f6a70dc7ac5c94105cd6e141e2da1d6e826dbf60 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 4 Mar 2022 18:41:03 +0200 Subject: [PATCH 12/23] Fix build --- .../org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt index 9d5190a93..21090bf5f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt @@ -17,7 +17,7 @@ class SuggestionsViewModel( settings: AppSettings, ) : MangaListViewModel(settings) { - private val headerModel = ListHeader(null, R.string.suggestions) + private val headerModel = ListHeader(null, R.string.suggestions, null) override val content = combine( repository.observeAll(), From eb7e2554301e393284a8959ad6c5deae42fc13d6 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 4 Mar 2022 19:09:58 +0200 Subject: [PATCH 13/23] Small ui fixes --- app/build.gradle | 8 ++++---- .../java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt | 3 ++- .../java/org/koitharu/kotatsu/main/ui/MainActivity.kt | 2 +- app/src/main/res/layout/activity_main.xml | 6 +++--- app/src/main/res/layout/fragment_favourites.xml | 1 - 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d8ccb45bf..de3d3fa5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,9 +87,9 @@ dependencies { //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1' - implementation 'androidx.room:room-runtime:2.4.1' - implementation 'androidx.room:room-ktx:2.4.1' - kapt 'androidx.room:room-compiler:2.4.1' + implementation 'androidx.room:room-runtime:2.4.2' + implementation 'androidx.room:room-ktx:2.4.2' + kapt 'androidx.room:room-compiler:2.4.2' implementation 'com.squareup.okhttp3:okhttp:4.9.3' implementation 'com.squareup.okio:okio:3.0.0' @@ -115,6 +115,6 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' - androidTestImplementation 'androidx.room:room-testing:2.4.1' + androidTestImplementation 'androidx.room:room-testing:2.4.2' androidTestImplementation 'com.google.truth:truth:1.1.3' } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt index 3e25b0bed..6b3b6a94b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt @@ -6,6 +6,7 @@ import androidx.room.PrimaryKey import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.utils.ext.longHashCode +import org.koitharu.kotatsu.utils.ext.toTitleCase @Entity(tableName = "tags") class TagEntity( @@ -18,7 +19,7 @@ class TagEntity( fun toMangaTag() = MangaTag( key = this.key, - title = this.title, + title = this.title.toTitleCase(), source = MangaSource.valueOf(this.source) ) diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 2b258bb53..329591adc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -292,7 +292,7 @@ class MainActivity : BaseActivity(), if (isLoading) { binding.fab.setImageDrawable(CircularProgressDrawable(this).also { it.setColorSchemeColors(R.color.kotatsu_onPrimaryContainer) - it.strokeWidth = resources.resolveDp(2f) + it.strokeWidth = resources.resolveDp(3.5f) it.start() }) } else { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b944dcc54..446d1ae2f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -22,7 +22,7 @@ android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@android:color/transparent" + android:background="@null" android:stateListAnimator="@null"> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_favourites.xml b/app/src/main/res/layout/fragment_favourites.xml index 47b8c515d..0f7e61994 100644 --- a/app/src/main/res/layout/fragment_favourites.xml +++ b/app/src/main/res/layout/fragment_favourites.xml @@ -10,7 +10,6 @@ android:id="@+id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content" - app:tabGravity="center" app:tabMode="scrollable" /> Date: Fri, 4 Mar 2022 20:10:29 +0200 Subject: [PATCH 14/23] Replace chapters dialog with bottom sheet --- .../kotatsu/reader/ui/ChaptersBottomSheet.kt | 125 ++++++++++++++++++ .../kotatsu/reader/ui/ChaptersDialog.kt | 99 -------------- .../kotatsu/reader/ui/ReaderActivity.kt | 4 +- app/src/main/res/layout/item_chapter.xml | 4 +- app/src/main/res/layout/sheet_chapters.xml | 34 +++++ 5 files changed, 164 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt create mode 100644 app/src/main/res/layout/sheet_chapters.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt new file mode 100644 index 000000000..1ec6ffa7a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt @@ -0,0 +1,125 @@ +package org.koitharu.kotatsu.reader.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.divider.MaterialDividerItemDecoration +import org.koin.android.ext.android.get +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseBottomSheet +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.model.MangaChapter +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.databinding.SheetChaptersBinding +import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter +import org.koitharu.kotatsu.details.ui.model.ChapterListItem +import org.koitharu.kotatsu.details.ui.model.toListItem +import org.koitharu.kotatsu.utils.ext.withArgs + +class ChaptersBottomSheet : BaseBottomSheet(), OnListItemClickListener { + + override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersBinding { + return SheetChaptersBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.toolbar.setNavigationOnClickListener { dismiss() } + if (!resources.getBoolean(R.bool.is_tablet)) { + binding.toolbar.navigationIcon = null + } + binding.recyclerView.addItemDecoration( + MaterialDividerItemDecoration(view.context, RecyclerView.VERTICAL) + ) + val chapters = arguments?.getParcelableArrayList(ARG_CHAPTERS) + if (chapters.isNullOrEmpty()) { + dismissAllowingStateLoss() + return + } + val currentId = requireArguments().getLong(ARG_CURRENT_ID, 0L) + val currentPosition = chapters.indexOfFirst { it.id == currentId } + val dateFormat = get().getDateFormat() + val items = chapters.mapIndexed { index, chapter -> + chapter.toListItem( + isCurrent = index == currentPosition, + isUnread = index > currentPosition, + isNew = false, + isMissing = false, + isDownloaded = false, + dateFormat = dateFormat, + ) + } + binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter -> + if (currentPosition >= 0) { + val targetPosition = (currentPosition - 1).coerceAtLeast(0) + adapter.setItems(items, Scroller(binding.recyclerView, targetPosition)) + } else { + adapter.items = items + } + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).also { + val behavior = (it as? BottomSheetDialog)?.behavior ?: return@also + behavior.addBottomSheetCallback( + object : BottomSheetBehavior.BottomSheetCallback() { + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit + + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_EXPANDED) { + binding.toolbar.setNavigationIcon(R.drawable.ic_cross) + } else { + binding.toolbar.navigationIcon = null + } + } + } + ) + } + + override fun onItemClick(item: ChapterListItem, view: View) { + ((parentFragment as? OnChapterChangeListener) ?: (activity as? OnChapterChangeListener))?.let { + dismiss() + it.onChapterChanged(item.chapter) + } + } + + fun interface OnChapterChangeListener { + + fun onChapterChanged(chapter: MangaChapter) + } + + private class Scroller(private val recyclerView: RecyclerView, private val position: Int) : Runnable { + override fun run() { + val offset = recyclerView.resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) / 2 + (recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(position, offset) + } + } + + companion object { + + private const val ARG_CHAPTERS = "chapters" + private const val ARG_CURRENT_ID = "current_id" + + private const val TAG = "ChaptersBottomSheet" + + fun show( + fm: FragmentManager, + chapters: List, + currentId: Long, + ) = ChaptersBottomSheet().withArgs(2) { + putParcelableArrayList(ARG_CHAPTERS, chapters.asArrayList()) + putLong(ARG_CURRENT_ID, currentId) + }.show(fm, TAG) + + private fun List.asArrayList(): ArrayList { + return this as? ArrayList ?: ArrayList(this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt deleted file mode 100644 index f091de3fd..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt +++ /dev/null @@ -1,99 +0,0 @@ -package org.koitharu.kotatsu.reader.ui - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.FragmentManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.divider.MaterialDividerItemDecoration -import org.koin.android.ext.android.get -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.AlertDialogFragment -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.model.MangaChapter -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.databinding.DialogChaptersBinding -import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter -import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.model.toListItem -import org.koitharu.kotatsu.utils.ext.withArgs - -class ChaptersDialog : AlertDialogFragment(), - OnListItemClickListener { - - override fun onInflateView( - inflater: LayoutInflater, - container: ViewGroup?, - ) = DialogChaptersBinding.inflate(inflater, container, false) - - override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { - builder.setTitle(R.string.chapters) - .setNegativeButton(R.string.close, null) - .setCancelable(true) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.recyclerViewChapters.addItemDecoration( - MaterialDividerItemDecoration(view.context, RecyclerView.VERTICAL) - ) - val chapters = arguments?.getParcelableArrayList(ARG_CHAPTERS) - if (chapters == null) { - dismissAllowingStateLoss() - return - } - val currentId = arguments?.getLong(ARG_CURRENT_ID, 0L) ?: 0L - val currentPosition = chapters.indexOfFirst { it.id == currentId } - val dateFormat = get().getDateFormat() - binding.recyclerViewChapters.adapter = ChaptersAdapter(this).apply { - setItems(chapters.mapIndexed { index, chapter -> - chapter.toListItem( - isCurrent = index == currentPosition, - isUnread = index > currentPosition, - isNew = false, - isMissing = false, - isDownloaded = false, - dateFormat = dateFormat, - ) - }) { - if (currentPosition >= 0) { - with(binding.recyclerViewChapters) { - (layoutManager as LinearLayoutManager).scrollToPositionWithOffset( - currentPosition, - height / 3 - ) - } - } - } - } - } - - override fun onItemClick(item: ChapterListItem, view: View) { - ((parentFragment as? OnChapterChangeListener) - ?: (activity as? OnChapterChangeListener))?.let { - dismiss() - it.onChapterChanged(item.chapter) - } - } - - fun interface OnChapterChangeListener { - - fun onChapterChanged(chapter: MangaChapter) - } - - companion object { - - private const val TAG = "ChaptersDialog" - - private const val ARG_CHAPTERS = "chapters" - private const val ARG_CURRENT_ID = "current_id" - - fun show(fm: FragmentManager, chapters: List, currentId: Long = 0L) = - ChaptersDialog().withArgs(2) { - putParcelableArrayList(ARG_CHAPTERS, ArrayList(chapters)) - putLong(ARG_CURRENT_ID, currentId) - }.show(fm, TAG) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index e4ee21e84..0f74db220 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -51,7 +51,7 @@ import org.koitharu.kotatsu.utils.anim.Motion import org.koitharu.kotatsu.utils.ext.* class ReaderActivity : BaseFullscreenActivity(), - ChaptersDialog.OnChapterChangeListener, + ChaptersBottomSheet.OnChapterChangeListener, GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback, ActivityResultCallback, ReaderControlDelegate.OnInteractionListener { @@ -152,7 +152,7 @@ class ReaderActivity : BaseFullscreenActivity(), startActivity(SimpleSettingsActivity.newReaderSettingsIntent(this)) } R.id.action_chapters -> { - ChaptersDialog.show( + ChaptersBottomSheet.show( supportFragmentManager, viewModel.manga?.chapters.orEmpty(), viewModel.getCurrentState()?.chapterId ?: 0L diff --git a/app/src/main/res/layout/item_chapter.xml b/app/src/main/res/layout/item_chapter.xml index fda5fe0f2..30f631e10 100644 --- a/app/src/main/res/layout/item_chapter.xml +++ b/app/src/main/res/layout/item_chapter.xml @@ -1,6 +1,7 @@ + android:src="@drawable/ic_new" + app:tint="?colorError" /> + + + + + + + + + + + \ No newline at end of file From 6eca4028ec95746a4ba0681e4a4838b1a19cfd2a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 5 Mar 2022 08:48:45 +0200 Subject: [PATCH 15/23] Dont recreate WebView on configuration changed #119 --- app/src/main/AndroidManifest.xml | 1 + .../kotatsu/browser/BrowserActivity.kt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a666a295e..d42998bc7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,6 +64,7 @@ (), BrowserCallback javaScriptEnabled = true } binding.webView.webViewClient = BrowserClient(this) + if (savedInstanceState != null) { + return + } val url = intent?.dataString if (url.isNullOrEmpty()) { finishAfterTransition() @@ -41,6 +44,16 @@ class BrowserActivity : BaseActivity(), BrowserCallback } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + binding.webView.saveState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + binding.webView.restoreState(savedInstanceState) + } + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.opt_browser, menu) return super.onCreateOptionsMenu(menu) @@ -82,6 +95,11 @@ class BrowserActivity : BaseActivity(), BrowserCallback binding.webView.onResume() } + override fun onDestroy() { + super.onDestroy() + binding.webView.destroy() + } + override fun onLoadingStateChanged(isLoading: Boolean) { binding.progressBar.isVisible = isLoading } From 4a88ecc549cb870a91d2289caf9a42e2e85e7c5f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 5 Mar 2022 09:01:35 +0200 Subject: [PATCH 16/23] Show WebView loading progress --- .../kotatsu/browser/BrowserActivity.kt | 1 + .../kotatsu/browser/ProgressChromeClient.kt | 31 ++++++++++++++++++ app/src/main/res/layout/activity_browser.xml | 32 +++++++++---------- 3 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/browser/ProgressChromeClient.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt index a46ce10fd..885e99b70 100644 --- a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt @@ -29,6 +29,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback javaScriptEnabled = true } binding.webView.webViewClient = BrowserClient(this) + binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar) if (savedInstanceState != null) { return } diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/ProgressChromeClient.kt b/app/src/main/java/org/koitharu/kotatsu/browser/ProgressChromeClient.kt new file mode 100644 index 000000000..0d890397e --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/browser/ProgressChromeClient.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.browser + +import android.webkit.WebChromeClient +import android.webkit.WebView +import androidx.core.view.isVisible +import com.google.android.material.progressindicator.BaseProgressIndicator +import org.koitharu.kotatsu.utils.ext.setIndeterminateCompat + +private const val PROGRESS_MAX = 100 + +class ProgressChromeClient( + private val progressIndicator: BaseProgressIndicator<*>, +) : WebChromeClient() { + + init { + progressIndicator.max = PROGRESS_MAX + } + + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + if (!progressIndicator.isVisible) { + return + } + if (newProgress in 1 until PROGRESS_MAX) { + progressIndicator.setIndeterminateCompat(false) + progressIndicator.setProgressCompat(newProgress.coerceAtMost(PROGRESS_MAX), true) + } else { + progressIndicator.setIndeterminateCompat(true) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_browser.xml b/app/src/main/res/layout/activity_browser.xml index b9a3c6992..729f00739 100644 --- a/app/src/main/res/layout/activity_browser.xml +++ b/app/src/main/res/layout/activity_browser.xml @@ -6,22 +6,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - - + + + + + + \ No newline at end of file From ff21d1c4ec4fec647b6ba8c4cb0c54b2a18dccb6 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sun, 6 Mar 2022 23:55:15 +0300 Subject: [PATCH 17/23] Fix incorrect arrow direction symbol --- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-nb-rNO/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index cf9fa937a..74c8d1132 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -158,7 +158,7 @@ Fallo en la comprobación de actualizaciones No hay actualizaciones disponibles Derecha a izquierda (←) - Preferir lector de derecha a izquierda (→) + Preferir lector de derecha a izquierda (←) Puedes configurar el modo de lectura para cada manga por separado Nueva categoría Crear incidencia en GitHub diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c303c19a4..2fd1eb2dc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -56,7 +56,7 @@ Signaler un problème sur GitHub Nouvelle catégorie Le mode de lecture peut être configuré séparément pour chaque série - Préférer le lecteur de droite à gauche (→) + Préférer le lecteur de droite à gauche (←) De droite à gauche (←) Aucune mise à jour disponible Échec de la recherche de mise à jour diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index bbc3fee2a..b58967f27 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -164,7 +164,7 @@ アップデートを見つける事が出来ませんでした 利用可能なアップデートはありません 右から左(←) - 右から左(→)の読書を好む + 右から左(←)の読書を好む フィードバック 4PDAに関する話題 開発者をサポートします(Yoomoneyが開きます) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index c94015221..3bb27d0d4 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -29,7 +29,7 @@ Opprett feilrapport på GitHub Lesemodus kan settes opp for hver serie Høyre-til-venstre (←) - Foretrekk høyre-til-venstre (→)-leser + Foretrekk høyre-til-venstre (←)-leser Ingen tilgjengelige oppdateringer Kunne ikke se etter oppdateringer Ser etter oppdateringer … diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 7418212e2..b5af919aa 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -215,7 +215,7 @@ Recente Outro armazenamento Tente reformular a consulta. - Prefira o leitor da direita para a esquerda (→) + Prefira o leitor da direita para a esquerda (←) Não disponível Tamanho: %s O que você ler será exibido aqui diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e16525c77..24df6ccf0 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -204,7 +204,7 @@ Arama sonuçları Ağ bekleniyor… Parolayı tekrarla - Sağdan sola (→) okuyucuyu tercih et + Sağdan sola (←) okuyucuyu tercih et Denetleme Yanlış parola GitHub\'da sorun oluştur diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 95b9f915e..4ff1827d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -164,7 +164,7 @@ Could not look for updates No updates available Right-to-left (←) - Prefer right-to-left (→) reader + Prefer right-to-left (←) reader Reading mode can be set up separately for each series New category Scale mode From afc9682d5350a73c9fe731f8b7ca33d2c4505adf Mon Sep 17 00:00:00 2001 From: Luiz-bro Date: Sun, 6 Mar 2022 20:59:22 +0100 Subject: [PATCH 18/23] Translated using Weblate (Portuguese) Currently translated at 100.0% (264 of 264 strings) Translated using Weblate (Portuguese) Currently translated at 98.4% (260 of 264 strings) Co-authored-by: Luiz-bro Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/ Translation: Kotatsu/Strings --- app/src/main/res/values-pt/strings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b5af919aa..5732181c8 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -256,4 +256,13 @@ Bloquear no NSFW Política de captura de tela Sempre bloquear + Sugira mangá com base em suas preferências + Todos os dados são analisados localmente neste dispositivo. Não há transferência de seus dados pessoais para nenhum serviço + Comece a ler mangá e você receberá sugestões personalizadas + Sugestões + Ativar sugestões + Não sugira mangá NSFW + Habilitado + Desabilitado + Não foi possível carregar a lista de gêneros \ No newline at end of file From ff4fe14f894a5fff171aa6b8157da01a38248ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sun, 6 Mar 2022 20:59:22 +0100 Subject: [PATCH 19/23] Translated using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (264 of 264 strings) Translated using Weblate (Turkish) Currently translated at 99.6% (263 of 264 strings) Co-authored-by: Oğuz Ersen Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/ Translation: Kotatsu/Strings --- app/src/main/res/values-tr/strings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 24df6ccf0..e4ac34869 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -257,4 +257,13 @@ Her zaman engelle İzin ver Yeni bölümleri denetle + Öneriler + Önerileri etkinleştir + Tercihlerinize göre manga önerileri alın + Tüm veriler aygıt üzerinde yerel olarak işlenir. Kişisel verilerinizin herhangi bir hizmete aktarılması söz konusu değildir + Manga okumaya başladıktan sonra kişiselleştirilmiş öneriler alacaksınız + Uygunsuz manga önerme + Etkin + Devre dışı + Türler listesi yüklenemiyor \ No newline at end of file From 5c3baa8575bbfd2fc51c5420e6fff638f4f542d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sun, 6 Mar 2022 20:59:23 +0100 Subject: [PATCH 20/23] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 99.2% (262 of 264 strings) Co-authored-by: Allan Nordhøy Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/ Translation: Kotatsu/Strings --- app/src/main/res/values-nb-rNO/strings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 3bb27d0d4..72d8c2df9 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -256,4 +256,13 @@ Blokker for sensurerbare Alltid blokker Skjermavbildningspraksis + Skru på forslag + Forslag + Du vil få personaliserte forslag når du begynner å lese manga + All data analyseres lokalt på enheten. Det er ingen overføring av persondata til noen tjenester. + Ikke foreslå sensurerbar manga + Kunne ikke laste inn sjangerliste + Foreslå manga basert på vaner + Påskrudd + Avskrudd \ No newline at end of file From 26e32ab584c14fda8106c227528e52bb2c5eaad5 Mon Sep 17 00:00:00 2001 From: kuragehime Date: Sun, 6 Mar 2022 20:59:23 +0100 Subject: [PATCH 21/23] Translated using Weblate (Japanese) Currently translated at 100.0% (264 of 264 strings) Co-authored-by: kuragehime Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/ Translation: Kotatsu/Strings --- app/src/main/res/values-ja/strings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index b58967f27..2f196fc8f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -256,4 +256,13 @@ 常にブロック スクリーンショットポリシー NSFWでブロック + 提案 + すべてのデータは、このデバイス上でローカルに分析されます。お客様のデータが他のサービスに転送されることはありません + サジェスト機能を有効 + あなたの好みに合わせて漫画を提案 + ジャンルリストを読み込めません + 無効 + マンガを読み始めると、個人的な提案を受けることができます + 有効 + NSFWのマンガを提案しない \ No newline at end of file From f52794e93cadbd6d14be204be9083f0c844b2efa Mon Sep 17 00:00:00 2001 From: mondstern Date: Sun, 6 Mar 2022 20:59:24 +0100 Subject: [PATCH 22/23] Translated using Weblate (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (264 of 264 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (264 of 264 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (264 of 264 strings) Translated using Weblate (Belarusian) Currently translated at 100.0% (264 of 264 strings) Co-authored-by: mondstern Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ Translation: Kotatsu/Strings --- app/src/main/res/values-be/strings.xml | 26 ++++++++++++++----- app/src/main/res/values-es/strings.xml | 27 +++++++++++++++----- app/src/main/res/values-nb-rNO/strings.xml | 4 +-- app/src/main/res/values-ru/strings.xml | 29 ++++++++++++---------- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 2831077f0..2155f5363 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -54,11 +54,11 @@ Аўтаматычна Старонкi Ачысціць - Вы ўпэўненыя, што жадаеце ачысціць гісторыю\? Гэта дзеянне нельга будзе адмяніць. + Вы ўпэўненыя, што жадаеце ачысціць гісторыю\? Выдаліць \"%s\" выдалена з гiсторыi \"%s\" выдалена з прылады - Дачакайцеся заканчэння загрузкі + Дачакайцеся заканчэння загрузкі… Захаваць старонку Старонка захавана Падзяліцца выявай @@ -79,8 +79,7 @@ Выдаліць мангу Налады чытання Гартанне старонак - Вы ўпэўненыя, што жадаеце выдаліць \"%s\" з прылады\? -\nГэта дзеянне нельга будзе адмяніць. + Вы ўпэўненыя, што жадаеце выдаліць \"%s\" з прылады\? Націск па краях Кнопкі гучнасці Працягнцуць @@ -207,7 +206,7 @@ Пароль павінен змяшчаць не менш за 4 сімвалы Схаваць загаловак пры прагортцы Пошук толькі па %s - Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты\? Гэта дзеянне нельга будзе адмяніць. + Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты\? Апісанне Падрабязна Некаторыя вытворцы могуць змяняць паводзіны сістэмы, што можа парушаць выкананне фонавых задач. @@ -227,7 +226,7 @@ У чарзе Ліцэнзія Аўтарскія правы і ліцэнзіі - Гэтыя людзі робяць Kotatsu лепш! + Гэтыя людзі робяць Kotatsu лепш Падзякі Калі вам падабаецца гэтая праграма, вы можаце дапамагчы фінансава з дапамогай ЮMoney (был. Яндекс.Деньги) Падтрымаць распрацоўшчыка @@ -251,4 +250,19 @@ Даступныя крыніцы Дынамічная тэма Ужывае тэму праграмы, заснаваную на каляровай палітры шпалер на прыладзе + Вылічэнні… + Імпарт мангі: %1$d of %2$d + Дазваляць + Палітыка скрыншотаў + Заўсёды блакуйце + Блок на NSFW + Немагчыма загрузіць спіс жанраў + Непрацаздольны + Уключаны + Не прапануйце мангу NSFW + Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы + Усе дадзеныя аналізуюцца лакальна на гэтай прыладзе. Перадача вашых персанальных дадзеных якім-небудзь сэрвісам не ажыццяўляецца + Прапануеце мангу, заснаваную на вашых перавагах + Уключыць прапановы + Прапанова \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 74c8d1132..a4c00187e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -54,7 +54,7 @@ De acuerdo al sistema Páginas Borrar - ¿Realmente quieres borrar todo tu historial de lectura\? Esta acción no se puede deshacer. + Borrar todo el historial de lectura de forma permanente\? Eliminar «%s» retirado del historial «%s» borrado del almacenamiento local @@ -72,7 +72,7 @@ Caché B|kB|MB|GB|TB Estándar - Webtoon + Sitio web Modo de lectura Tamaño de la cuadrícula Buscar en %s @@ -177,7 +177,7 @@ Preparando… Archivo no encontrado Todos los datos fueron restaurados con éxito - Los datos fueron restaurados, pero hay errores. + Los datos fueron restaurados, pero hay errores Puedes crear una copia de seguridad de tu historial y favoritos para restaurarla Ahora mismo Ayer @@ -213,7 +213,7 @@ Si te gusta esta aplicación, puedes ayudar económicamente a través de Yoomoney (ex. Yandex.Money) Apoyar al desarrollador Buscar sólo en %s - Todas estas personas hicieron que Kotatsu fuera mejor. + Todas estas personas hicieron que Kotatsu fuera mejor Licencia Derechos de autor y licencias Falta un capítulo @@ -231,7 +231,7 @@ Otro Géneros Intenta reformular la consulta. - ¿Realmente quiere eliminar todas las consultas de búsqueda recientes\? Esta acción no se puede deshacer. + ¿Realmente quiere eliminar todas las consultas de búsqueda recientes\? Terminado En curso Ocultar la barra de herramientas al desplazarse @@ -243,11 +243,26 @@ Algunos fabricantes pueden cambiar el comportamiento del sistema, lo que podría interrumpir las tareas en segundo plano. El nombre no debe estar vacío No se admite iniciar sesión en %s - Serás desconectado de todas las fuentes. + Serás desconectado de todas las fuentes Excluye manga NSFW del historial Mostrar los números de páginas Fuentes activadas Fuentes disponibles Tema dinámico Aplica un tema creado a partir del esquema de colores de su fondo de pantalla + Informática… + Importando manga: %1$d de %2$d + Política de capturas de pantalla + Permitir + Bloquear siempre + Sugerencias + Activar sugerencias + Sugiere mangas según tus preferencias + Todos los datos se analizan localmente en este dispositivo. No hay transferencia de sus datos personales a ningún servicio + Empieza a leer manga y recibirás sugerencias personalizadas + No sugerir manga NSFW + Activado + Desactivado + No se puede cargar la lista de géneros + Bloqueo en NSFW \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 72d8c2df9..2078c044f 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -251,7 +251,7 @@ Dynamisk tema Bruker et tema basert på fargene til bakgrunnen din Beregner … - Importerer manga: %1$d av %2$d + Importere manga: %1$d av %2$d Tillat Blokker for sensurerbare Alltid blokker @@ -259,7 +259,7 @@ Skru på forslag Forslag Du vil få personaliserte forslag når du begynner å lese manga - All data analyseres lokalt på enheten. Det er ingen overføring av persondata til noen tjenester. + Alle data analyseres lokalt på denne enheten. Det er ingen overføring av dine personlige data til noen tjenester Ikke foreslå sensurerbar manga Kunne ikke laste inn sjangerliste Foreslå manga basert på vaner diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4d02b2a56..5b75987f2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -249,17 +249,20 @@ Доступные источники Динамическая тема Применяет тему приложения, основанную на цветовой палитре обоев на устройстве - Разрешить скриншоты - Разрешить - Запретить для NSFW - Запретить всегда - Рекомендации - Включить рекомендации - Предлагать мангу на основе Ваших предпочтений - Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы - Начните читать мангу, чтобы получать персональные предложения - Не предлагать NSFW мангу - Включено - Выключено - Не удалось загрузить список жанров + Политика скриншотов + Разрешить + Запретить для NSFW + Всегда блокировать + Рекомендации + Включить рекомендации + Предлагать мангу на основе Ваших предпочтений + Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы + Начните читать мангу, чтобы получать персональные предложения + Не предлагать NSFW мангу + Включено + Выключено + Не удалось загрузить список жанров + Вычисления… + Создать проблему на GitHub + Импорт манги: %1$d из %2$d \ No newline at end of file From 6a40a388b3ed0dd6eb09d5fcd095870c4cdbbc01 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sun, 6 Mar 2022 20:59:24 +0100 Subject: [PATCH 23/23] Translated using Weblate (Finnish) Currently translated at 100.0% (264 of 264 strings) Translated using Weblate (French) Currently translated at 100.0% (264 of 264 strings) Translated using Weblate (Italian) Currently translated at 100.0% (264 of 264 strings) Translated using Weblate (German) Currently translated at 100.0% (264 of 264 strings) Co-authored-by: J. Lavoie Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/ Translation: Kotatsu/Strings --- app/src/main/res/values-de/strings.xml | 9 +++++++++ app/src/main/res/values-fi/strings.xml | 9 +++++++++ app/src/main/res/values-fr/strings.xml | 9 +++++++++ app/src/main/res/values-it/strings.xml | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 147962f9d..036e2db27 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -256,4 +256,13 @@ Bildschirmfoto-Richtlinie Für NSFW blockieren Immer blockieren + Vorschläge + Vorschläge einschalten + Manga nach deinen Vorlieben vorschlagen + Alle Daten werden lokal auf diesem Gerät ausgewertet. Es findet keine Übertragung Ihrer persönlichen Daten an andere Dienste statt + Keine NSFW-Manga vorschlagen + Aktiviert + Fang an, Manga zu lesen und du bekommst personalisierte Vorschläge + Deaktiviert + Liste der Genres kann nicht geladen werden \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 19f03a87d..960898a61 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -256,4 +256,13 @@ Lasketaan… Käytettävissä olevat lähteet Dynaaminen teema + Ehdotukset + Ehdota mangaa mieltymystesi perusteella + Kaikki tiedot analysoidaan paikallisesti tässä laitteessa. Henkilötietojasi ei siirretä mihinkään palveluihin + Ota ehdotukset käyttöön + Älä ehdota NSFW-mangaa + Aloita mangan lukeminen ja saat henkilökohtaisia ehdotuksia + Käytössä + Pois päältä + Genreluetteloa ei voida ladata \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2fd1eb2dc..3824772ec 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -256,4 +256,13 @@ Toujours bloquer Politique relative aux captures d\'écran Autoriser + Suggestions + Ne pas suggérer de mangas osés + Activer les suggestions + Suggérer des mangas en fonction de vos préférences + Toutes les données sont analysées localement sur cet appareil. Vos données personnelles ne sont pas transférées à d\'autres services + Commencez à lire des mangas et vous recevrez des suggestions personnalisées + Impossible de charger la liste des genres + Activé + Désactivé \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 8cbe33553..71e33d82d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -256,4 +256,13 @@ Permetti Blocca per NSFW Blocca sempre + Impossibile caricare la lista dei generi + Abilita i suggerimenti + Suggerisci manga in base alle tue preferenze + Tutti i dati sono analizzati localmente su questo dispositivo. Non c\'è trasferimento dei suoi dati personali a nessun servizio + Inizia a leggere manga e riceverai suggerimenti personalizzati + Suggerimenti + Abilitato + Disabilitato + Non suggerire manga NSFW \ No newline at end of file