diff --git a/app/build.gradle b/app/build.gradle index f41881156..8221b897a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 1009 - versionName = '8.1.3' + versionCode = 1010 + versionName = '8.1.4' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt index d701edaf1..6fa5360dc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt @@ -17,9 +17,9 @@ abstract class BookmarksDao { @Transaction @Query( - "SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent", + "SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent LIMIT :limit OFFSET :offset", ) - abstract suspend fun findAll(): Map> + abstract suspend fun findAll(offset: Int, limit: Int): Map> @Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page ORDER BY percent") abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 5a63fd5aa..c1f30c0d1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -28,7 +28,7 @@ class BackupRepository @Inject constructor( var offset = 0 val entry = BackupEntry(BackupEntry.Name.HISTORY, JSONArray()) while (true) { - val history = db.getHistoryDao().findAll(offset, PAGE_SIZE) + val history = db.getHistoryDao().findAll(offset = offset, limit = PAGE_SIZE) if (history.isEmpty()) { break } @@ -59,7 +59,7 @@ class BackupRepository @Inject constructor( var offset = 0 val entry = BackupEntry(BackupEntry.Name.FAVOURITES, JSONArray()) while (true) { - val favourites = db.getFavouritesDao().findAllRaw(offset, PAGE_SIZE) + val favourites = db.getFavouritesDao().findAllRaw(offset = offset, limit = PAGE_SIZE) if (favourites.isEmpty()) { break } @@ -78,19 +78,26 @@ class BackupRepository @Inject constructor( } suspend fun dumpBookmarks(): BackupEntry { + var offset = 0 val entry = BackupEntry(BackupEntry.Name.BOOKMARKS, JSONArray()) - val all = db.getBookmarksDao().findAll() - for ((m, b) in all) { - val json = JSONObject() - val manga = JsonSerializer(m.manga).toJson() - json.put("manga", manga) - val tags = JSONArray() - m.tags.forEach { tags.put(JsonSerializer(it).toJson()) } - json.put("tags", tags) - val bookmarks = JSONArray() - b.forEach { bookmarks.put(JsonSerializer(it).toJson()) } - json.put("bookmarks", bookmarks) - entry.data.put(json) + while (true) { + val bookmarks = db.getBookmarksDao().findAll(offset = offset, limit = PAGE_SIZE) + if (bookmarks.isEmpty()) { + break + } + offset += bookmarks.size + for ((m, b) in bookmarks) { + val json = JSONObject() + val manga = JsonSerializer(m.manga).toJson() + json.put("manga", manga) + val tags = JSONArray() + m.tags.forEach { tags.put(JsonSerializer(it).toJson()) } + json.put("tags", tags) + val bookmarks = JSONArray() + b.forEach { bookmarks.put(JsonSerializer(it).toJson()) } + json.put("bookmarks", bookmarks) + entry.data.put(json) + } } return entry } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 1943702bb..d64c34e03 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -151,6 +151,8 @@ fun Manga.chaptersCount(): Int { return max } +fun Manga.isNsfw(): Boolean = contentRating == ContentRating.ADULT || source.isNsfw() + fun MangaListFilter.getSummary() = buildSpannedString { if (!query.isNullOrEmpty()) { append(query) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt index 3643a4c9e..2e6979c10 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt @@ -100,7 +100,11 @@ abstract class ChaptersPagesViewModel( }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) val bookmarks = mangaDetails.flatMapLatest { - if (it != null) bookmarksRepository.observeBookmarks(it.toManga()) else flowOf(emptyList()) + if (it != null) { + bookmarksRepository.observeBookmarks(it.toManga()).withErrorHandling() + } else { + flowOf(emptyList()) + } }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList()) val chapters = combine( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt index 9b3d0b651..ecf6af769 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt @@ -25,6 +25,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ErrorReporterReceiver import org.koitharu.kotatsu.core.LocalizedAppContext import org.koitharu.kotatsu.core.model.LocalMangaSource +import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.core.util.ext.isReportable @@ -140,10 +141,10 @@ class DownloadNotificationFactory @AssistedInject constructor( builder.setSubText(null) builder.setShowWhen(false) builder.setVisibility( - if (state != null && state.manga.isNsfw) { - NotificationCompat.VISIBILITY_PRIVATE + if (state != null && state.manga.isNsfw()) { + NotificationCompat.VISIBILITY_SECRET } else { - NotificationCompat.VISIBILITY_PUBLIC + NotificationCompat.VISIBILITY_PRIVATE }, ) when { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index a2c4108b6..2d8142cf8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -352,7 +352,7 @@ class SuggestionsWorker @AssistedInject constructor( ) setAutoCancel(true) setCategory(NotificationCompat.CATEGORY_RECOMMENDATION) - setVisibility(if (manga.isNsfw) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC) + setVisibility(if (manga.isNsfw()) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PRIVATE) setShortcutId(manga.id.toString()) priority = NotificationCompat.PRIORITY_DEFAULT diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt index 07f04a241..009e77f00 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -27,17 +27,17 @@ abstract class TracksDao : MangaQueryBuilder.ConditionCallback { @Query("SELECT * FROM tracks WHERE manga_id = :mangaId") abstract suspend fun find(mangaId: Long): TrackEntity? - @Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId") - abstract suspend fun findNewChapters(mangaId: Long): Int? + @Query("SELECT IFNULL(chapters_new,0) FROM tracks WHERE manga_id = :mangaId") + abstract suspend fun findNewChapters(mangaId: Long): Int @Query("SELECT COUNT(*) FROM tracks") abstract suspend fun getTracksCount(): Int - @Query("SELECT chapters_new FROM tracks") - abstract fun observeNewChapters(): Flow> + @Query("SELECT COUNT(*) FROM tracks WHERE chapters_new > 0") + abstract fun observeUpdateMangaCount(): Flow - @Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId") - abstract fun observeNewChapters(mangaId: Long): Flow + @Query("SELECT IFNULL(chapters_new, 0) FROM tracks WHERE manga_id = :mangaId") + abstract fun observeNewChapters(mangaId: Long): Flow @Transaction @Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC") diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index dfe81770f..b2becc7ae 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -5,7 +5,6 @@ import androidx.room.withTransaction import dagger.Reusable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.toManga @@ -39,16 +38,16 @@ class TrackingRepository @Inject constructor( private var isGcCalled = AtomicBoolean(false) suspend fun getNewChaptersCount(mangaId: Long): Int { - return db.getTracksDao().findNewChapters(mangaId) ?: 0 + return db.getTracksDao().findNewChapters(mangaId) } fun observeNewChaptersCount(mangaId: Long): Flow { - return db.getTracksDao().observeNewChapters(mangaId).map { it ?: 0 } + return db.getTracksDao().observeNewChapters(mangaId) } @Deprecated("") fun observeUpdatedMangaCount(): Flow { - return db.getTracksDao().observeNewChapters().map { list -> list.count { it > 0 } } + return db.getTracksDao().observeUpdateMangaCount() .onStart { gcIfNotCalled() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt index a995979b0..8c3f721f6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt @@ -21,6 +21,7 @@ class TrackerDebugViewModel @Inject constructor( val content = db.getTracksDao().observeAll() .map { it.toUiList() } + .withErrorHandling() .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) private fun List.toUiList(): List = map { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationHelper.kt index 54bb294a9..6e097ecc6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationHelper.kt @@ -7,7 +7,7 @@ import android.content.Context import android.os.Build import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC +import androidx.core.app.NotificationCompat.VISIBILITY_PRIVATE import androidx.core.app.NotificationCompat.VISIBILITY_SECRET import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat @@ -17,12 +17,14 @@ import coil3.request.ImageRequest import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.LocalizedAppContext import org.koitharu.kotatsu.core.model.getLocalizedTitle +import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull +import org.koitharu.kotatsu.parsers.model.ContentRating import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import javax.inject.Inject @@ -51,7 +53,7 @@ class TrackerNotificationHelper @Inject constructor( if (newChapters.isEmpty() || !applicationContext.checkNotificationPermission(CHANNEL_ID)) { return null } - if (manga.isNsfw && (settings.isTrackerNsfwDisabled || settings.isNsfwContentDisabled)) { + if (manga.isNsfw() && (settings.isTrackerNsfwDisabled || settings.isNsfwContentDisabled)) { return null } val id = manga.url.hashCode() @@ -92,7 +94,7 @@ class TrackerNotificationHelper @Inject constructor( false, ), ) - setVisibility(if (manga.isNsfw) VISIBILITY_SECRET else VISIBILITY_PUBLIC) + setVisibility(if (manga.isNsfw()) VISIBILITY_SECRET else VISIBILITY_PRIVATE) setShortcutId(manga.id.toString()) applyCommonSettings(this) } @@ -127,6 +129,13 @@ class TrackerNotificationHelper @Inject constructor( setNumber(newChaptersCount) setGroup(GROUP_NEW_CHAPTERS) setGroupSummary(true) + setVisibility( + if (notifications.any { it.manga.isNsfw() }) { + VISIBILITY_SECRET + } else { + VISIBILITY_PRIVATE + }, + ) val intent = AppRouter.mangaUpdatesIntent(applicationContext) setContentIntent( PendingIntentCompat.getActivity( diff --git a/app/src/main/res/raw/tags_warnlist b/app/src/main/res/raw/tags_warnlist index ec6736948..5f89959f5 100644 --- a/app/src/main/res/raw/tags_warnlist +++ b/app/src/main/res/raw/tags_warnlist @@ -1,119 +1,107 @@ -yaoi -yuri -trap -traps -guro -furry -loli -incest -tentacles -shemale -scat -яой -юри -трап -копро -гуро -тентакли -футанари -инцест -boys' love -girls' love +amputation +amputee +anal birth +anal torture bdsm -futanari -ntr -coprophagia -unbirth -rape -mother +beast +beastiality +bestiality +birth +blackmail +blood +body horror +bondage +boys' love +brother +bukkake +cannibalism +cbt +choking +coprophagia +degradation +diapers +drugs +egg laying +electrical play +electro +electro play +enema +extreme father -sister +femdom +force +full censorship +furry +futanari +gang rape +gangbang +gangbang rape +gender bender +girls' love +guro +human pet +humiliation +hypno +incest +inflation +insect +inseki +knife play +loli +lolicon +machine +mind break +mindbreak +molestation +mosaic +mother +mutilation +necrophila +necrophilia +netorase +nipple torture +non-consensual +ntr +orgasm denial +parasite +piercing +prolapse +prostitution +public use +puke +puppy play +rape +ryona +scar +scat +shemale shota shotacon -mother -father -brother -rape -blackmail -lolicon -toddlercon -birth -mind break -ryona -beastiality -urination +sister slave -human pet -amputee -amputation -gender bender +slavery +snuff +tentacles +toddlercon +torture trans transgender -full censorship -mosaic -gang rape -furry -inseki -necrophila -prostitution -torture -vore -vaginal birth -parasite -snuff -cannibalism -anal birth -netorase -guro -bestiality -mutilation -vomit -inflation -necrophilia -insect -enema -diapers -beast -parasite -body horror -cbt -piercing -blood -non-consensual -machine -egg laying -femdom -humiliation -public use -bukkake -gangbang +trap +traps +unbirth urination -incest -lolicon -drugs -slavery -degradation -bondage -watersports -choking -orgasm denial -beastiality -electrical play -hypno -force -molestation -anal torture -prolapse -electro -knife play -scar -degradation -puke -nipple torture -extreme +vaginal birth violent -degradation -gangbang rape -mindbreak -puppy play -electro play +vomit +vore +watersports +yaoi +yuri +гуро +инцест +копро +тентакли +трап +футанари +юри +яой diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f94645e5a..046ee8a0d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ material = "1.13.0-alpha13" moshi = "1.15.2" okhttp = "4.12.0" okio = "3.11.0" -parsers = "e874837efb" +parsers = "b165a0d611" preference = "1.2.1" recyclerview = "1.4.0" room = "2.7.1"