Merge remote-tracking branch 'upstream/master'

This commit is contained in:
jmir1 2022-07-02 21:24:18 +02:00
commit 35278e9858
88 changed files with 1213 additions and 1190 deletions

View file

@ -19,6 +19,7 @@ shortcutHelper.setFilePath("./shortcuts.xml")
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android {
namespace = "eu.kanade.tachiyomi"
compileSdk = AndroidConfig.compileSdk
ndkVersion = AndroidConfig.ndk

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.kanade.tachiyomi">
xmlns:tools="http://schemas.android.com/tools" >
<!-- Internet -->
<uses-permission android:name="android.permission.INTERNET" />

View file

@ -1,10 +1,17 @@
package eu.kanade.core.util
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import rx.Emitter
import rx.Observable
import rx.Observer
import kotlin.coroutines.CoroutineContext
fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val observer = object : Observer<T> {
@ -23,3 +30,32 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val subscription = subscribe(observer)
awaitClose { subscription.unsubscribe() }
}
fun <T : Any> Flow<T>.asObservable(
context: CoroutineContext = Dispatchers.Unconfined,
backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE,
): Observable<T> {
return Observable.create(
{ emitter ->
/*
* ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
* asObservable is already invoked from unconfined
*/
val job = GlobalScope.launch(context = context, start = CoroutineStart.ATOMIC) {
try {
collect { emitter.onNext(it) }
emitter.onCompleted()
} catch (e: Throwable) {
// Ignore `CancellationException` as error, since it indicates "normal cancellation"
if (e !is CancellationException) {
emitter.onError(e)
} else {
emitter.onCompleted()
}
}
}
emitter.setCancellation { job.cancel() }
},
backpressureMode,
)
}

View file

@ -46,7 +46,7 @@ class AnimeRepositoryImpl(
}
}
override suspend fun moveAnimeToCategories(animeId: Long, categoryIds: List<Long>) {
override suspend fun setAnimeCategories(animeId: Long, categoryIds: List<Long>) {
handler.await(inTransaction = true) {
animes_categoriesQueries.deleteAnimeCategoryByAnimeId(animeId)
categoryIds.map { categoryId ->
@ -57,31 +57,47 @@ class AnimeRepositoryImpl(
override suspend fun update(update: AnimeUpdate): Boolean {
return try {
handler.await {
animesQueries.update(
source = update.source,
url = update.url,
artist = update.artist,
author = update.author,
description = update.description,
genre = update.genre?.let(listOfStringsAdapter::encode),
title = update.title,
status = update.status,
thumbnailUrl = update.thumbnailUrl,
favorite = update.favorite?.toLong(),
lastUpdate = update.lastUpdate,
initialized = update.initialized?.toLong(),
viewer = update.viewerFlags,
episodeFlags = update.episodeFlags,
coverLastModified = update.coverLastModified,
dateAdded = update.dateAdded,
animeId = update.id,
)
}
partialUpdate(update)
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
override suspend fun updateAll(values: List<AnimeUpdate>): Boolean {
return try {
partialUpdate(*values.toTypedArray())
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
private suspend fun partialUpdate(vararg values: AnimeUpdate) {
handler.await(inTransaction = true) {
values.forEach { value ->
animesQueries.update(
source = value.source,
url = value.url,
artist = value.artist,
author = value.author,
description = value.description,
genre = value.genre?.let(listOfStringsAdapter::encode),
title = value.title,
status = value.status,
thumbnailUrl = value.thumbnailUrl,
favorite = value.favorite?.toLong(),
lastUpdate = value.lastUpdate,
initialized = value.initialized?.toLong(),
viewer = value.viewerFlags,
episodeFlags = value.episodeFlags,
coverLastModified = value.coverLastModified,
dateAdded = value.dateAdded,
animeId = value.id,
)
}
}
}
}

View file

@ -15,7 +15,13 @@ class AnimeTrackRepositoryImpl(
}
}
override suspend fun subscribeAnimeTracksByAnimeId(animeId: Long): Flow<List<AnimeTrack>> {
override fun getAnimeTracksAsFlow(): Flow<List<AnimeTrack>> {
return handler.subscribeToList {
anime_syncQueries.getAnimeTracks(animetrackMapper)
}
}
override fun getAnimeTracksByAnimeIdAsFlow(animeId: Long): Flow<List<AnimeTrack>> {
return handler.subscribeToList {
anime_syncQueries.getTracksByAnimeId(animeId, animetrackMapper)
}

View file

@ -15,6 +15,18 @@ class CategoryRepositoryImpl(
return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) }
}
override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
}
}
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
return handler.subscribeToList {
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
}
}
@Throws(DuplicateNameException::class)
override suspend fun insert(name: String, order: Long) {
if (checkDuplicateName(name)) throw DuplicateNameException(name)
@ -48,12 +60,6 @@ class CategoryRepositoryImpl(
}
}
override suspend fun getCategoriesForManga(mangaId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
}
}
override suspend fun checkDuplicateName(name: String): Boolean {
return handler
.awaitList { categoriesQueries.getCategories() }

View file

@ -15,6 +15,18 @@ class CategoryRepositoryImplAnime(
return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) }
}
override suspend fun getCategoriesByAnimeId(animeId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getCategoriesByAnimeId(animeId, categoryMapper)
}
}
override fun getCategoriesByAnimeIdAsFlow(animeId: Long): Flow<List<Category>> {
return handler.subscribeToList {
categoriesQueries.getCategoriesByAnimeId(animeId, categoryMapper)
}
}
@Throws(DuplicateNameException::class)
override suspend fun insert(name: String, order: Long) {
if (checkDuplicateName(name)) throw DuplicateNameException(name)
@ -48,12 +60,6 @@ class CategoryRepositoryImplAnime(
}
}
override suspend fun getCategoriesForAnime(animeId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getCategoriesByAnimeId(animeId, categoryMapper)
}
}
override suspend fun checkDuplicateName(name: String): Boolean {
return handler
.awaitList { categoriesQueries.getCategories() }

View file

@ -92,7 +92,11 @@ class ChapterRepositoryImpl(
return handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) }
}
override fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>> {
override suspend fun getChapterById(id: Long): Chapter? {
return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, chapterMapper) }
}
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>> {
return handler.subscribeToList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) }
}
}

View file

@ -95,6 +95,10 @@ class EpisodeRepositoryImpl(
return handler.awaitList { episodesQueries.getEpisodesByAnimeId(animeId, episodeMapper) }
}
override suspend fun getEpisodeById(id: Long): Episode? {
return handler.awaitOneOrNull { episodesQueries.getEpisodeById(id, episodeMapper) }
}
override fun getEpisodeByAnimeIdAsFlow(animeId: Long): Flow<List<Episode>> {
return handler.subscribeToList { episodesQueries.getEpisodesByAnimeId(animeId, episodeMapper) }
}

View file

@ -46,7 +46,7 @@ class MangaRepositoryImpl(
}
}
override suspend fun moveMangaToCategories(mangaId: Long, categoryIds: List<Long>) {
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
handler.await(inTransaction = true) {
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
categoryIds.map { categoryId ->
@ -57,31 +57,47 @@ class MangaRepositoryImpl(
override suspend fun update(update: MangaUpdate): Boolean {
return try {
handler.await {
mangasQueries.update(
source = update.source,
url = update.url,
artist = update.artist,
author = update.author,
description = update.description,
genre = update.genre?.let(listOfStringsAdapter::encode),
title = update.title,
status = update.status,
thumbnailUrl = update.thumbnailUrl,
favorite = update.favorite?.toLong(),
lastUpdate = update.lastUpdate,
initialized = update.initialized?.toLong(),
viewer = update.viewerFlags,
chapterFlags = update.chapterFlags,
coverLastModified = update.coverLastModified,
dateAdded = update.dateAdded,
mangaId = update.id,
)
}
partialUpdate(update)
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
override suspend fun updateAll(values: List<MangaUpdate>): Boolean {
return try {
partialUpdate(*values.toTypedArray())
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
private suspend fun partialUpdate(vararg values: MangaUpdate) {
handler.await(inTransaction = true) {
values.forEach { value ->
mangasQueries.update(
source = value.source,
url = value.url,
artist = value.artist,
author = value.author,
description = value.description,
genre = value.genre?.let(listOfStringsAdapter::encode),
title = value.title,
status = value.status,
thumbnailUrl = value.thumbnailUrl,
favorite = value.favorite?.toLong(),
lastUpdate = value.lastUpdate,
initialized = value.initialized?.toLong(),
viewer = value.viewerFlags,
chapterFlags = value.chapterFlags,
coverLastModified = value.coverLastModified,
dateAdded = value.dateAdded,
mangaId = value.id,
)
}
}
}
}

View file

@ -15,7 +15,13 @@ class TrackRepositoryImpl(
}
}
override suspend fun subscribeTracksByMangaId(mangaId: Long): Flow<List<Track>> {
override fun getTracksAsFlow(): Flow<List<Track>> {
return handler.subscribeToList {
manga_syncQueries.getTracks(trackMapper)
}
}
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
return handler.subscribeToList {
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
}

View file

@ -48,18 +48,20 @@ import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.GetCategoriesAnime
import eu.kanade.domain.category.interactor.InsertCategory
import eu.kanade.domain.category.interactor.InsertCategoryAnime
import eu.kanade.domain.category.interactor.MoveAnimeToCategories
import eu.kanade.domain.category.interactor.MoveMangaToCategories
import eu.kanade.domain.category.interactor.SetAnimeCategories
import eu.kanade.domain.category.interactor.SetMangaCategories
import eu.kanade.domain.category.interactor.UpdateCategory
import eu.kanade.domain.category.interactor.UpdateCategoryAnime
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
import eu.kanade.domain.chapter.interactor.GetChapter
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.ShouldUpdateDbChapter
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.domain.episode.interactor.GetEpisode
import eu.kanade.domain.episode.interactor.GetEpisodeByAnimeId
import eu.kanade.domain.episode.interactor.ShouldUpdateDbEpisode
import eu.kanade.domain.episode.interactor.SyncEpisodesWithSource
@ -120,9 +122,10 @@ class DomainModule : InjektModule {
addFactory { ResetViewerFlagsAnime(get()) }
addFactory { SetAnimeEpisodeFlags(get()) }
addFactory { UpdateAnime(get()) }
addFactory { MoveAnimeToCategories(get()) }
addFactory { SetAnimeCategories(get()) }
addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) }
addFactory { GetEpisode(get()) }
addFactory { GetEpisodeByAnimeId(get()) }
addFactory { UpdateEpisode(get()) }
addFactory { ShouldUpdateDbEpisode() }
@ -150,7 +153,7 @@ class DomainModule : InjektModule {
addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
addFactory { UpdateManga(get()) }
addFactory { MoveMangaToCategories(get()) }
addFactory { SetMangaCategories(get()) }
addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) }
addFactory { DeleteAnimeTrack(get()) }
@ -163,6 +166,7 @@ class DomainModule : InjektModule {
addFactory { InsertTrack(get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) }
addFactory { UpdateChapter(get()) }
addFactory { ShouldUpdateDbChapter() }

View file

@ -20,6 +20,10 @@ class UpdateAnime(
return animeRepository.update(animeUpdate)
}
suspend fun awaitAll(values: List<AnimeUpdate>): Boolean {
return animeRepository.updateAll(values)
}
suspend fun awaitUpdateFromSource(
localAnime: Anime,
remoteAnime: AnimeInfo,

View file

@ -18,7 +18,9 @@ interface AnimeRepository {
suspend fun resetViewerFlags(): Boolean
suspend fun moveAnimeToCategories(animeId: Long, categoryIds: List<Long>)
suspend fun setAnimeCategories(animeId: Long, categoryIds: List<Long>)
suspend fun update(update: AnimeUpdate): Boolean
suspend fun updateAll(values: List<AnimeUpdate>): Boolean
}

View file

@ -19,7 +19,11 @@ class GetAnimeTracks(
}
}
suspend fun subscribe(animeId: Long): Flow<List<AnimeTrack>> {
return animetrackRepository.subscribeAnimeTracksByAnimeId(animeId)
fun subscribe(): Flow<List<AnimeTrack>> {
return animetrackRepository.getAnimeTracksAsFlow()
}
fun subscribe(animeId: Long): Flow<List<AnimeTrack>> {
return animetrackRepository.getAnimeTracksByAnimeIdAsFlow(animeId)
}
}

View file

@ -7,7 +7,9 @@ interface AnimeTrackRepository {
suspend fun getAnimeTracksByAnimeId(animeId: Long): List<AnimeTrack>
suspend fun subscribeAnimeTracksByAnimeId(animeId: Long): Flow<List<AnimeTrack>>
fun getAnimeTracksAsFlow(): Flow<List<AnimeTrack>>
fun getAnimeTracksByAnimeIdAsFlow(mangaId: Long): Flow<List<AnimeTrack>>
suspend fun delete(animeId: Long, syncId: Long)

View file

@ -12,7 +12,11 @@ class GetCategories(
return categoryRepository.getAll()
}
fun subscribe(mangaId: Long): Flow<List<Category>> {
return categoryRepository.getCategoriesByMangaIdAsFlow(mangaId)
}
suspend fun await(mangaId: Long): List<Category> {
return categoryRepository.getCategoriesForManga(mangaId)
return categoryRepository.getCategoriesByMangaId(mangaId)
}
}

View file

@ -12,7 +12,11 @@ class GetCategoriesAnime(
return categoryRepository.getAll()
}
fun subscribe(animeId: Long): Flow<List<Category>> {
return categoryRepository.getCategoriesByAnimeIdAsFlow(animeId)
}
suspend fun await(animeId: Long): List<Category> {
return categoryRepository.getCategoriesForAnime(animeId)
return categoryRepository.getCategoriesByAnimeId(animeId)
}
}

View file

@ -4,13 +4,13 @@ import eu.kanade.domain.anime.repository.AnimeRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class MoveAnimeToCategories(
private val mangaRepository: AnimeRepository,
class SetAnimeCategories(
private val animeRepository: AnimeRepository,
) {
suspend fun await(mangaId: Long, categoryIds: List<Long>) {
suspend fun await(animeId: Long, categoryIds: List<Long>) {
try {
mangaRepository.moveAnimeToCategories(mangaId, categoryIds)
animeRepository.setAnimeCategories(animeId, categoryIds)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}

View file

@ -4,13 +4,13 @@ import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class MoveMangaToCategories(
class SetMangaCategories(
private val mangaRepository: MangaRepository,
) {
suspend fun await(mangaId: Long, categoryIds: List<Long>) {
try {
mangaRepository.moveMangaToCategories(mangaId, categoryIds)
mangaRepository.setMangaCategories(mangaId, categoryIds)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}

View file

@ -8,6 +8,10 @@ interface CategoryRepository {
fun getAll(): Flow<List<Category>>
suspend fun getCategoriesByMangaId(mangaId: Long): List<Category>
fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>>
@Throws(DuplicateNameException::class)
suspend fun insert(name: String, order: Long)
@ -16,8 +20,6 @@ interface CategoryRepository {
suspend fun delete(categoryId: Long)
suspend fun getCategoriesForManga(mangaId: Long): List<Category>
suspend fun checkDuplicateName(name: String): Boolean
}

View file

@ -8,6 +8,10 @@ interface CategoryRepositoryAnime {
fun getAll(): Flow<List<Category>>
suspend fun getCategoriesByAnimeId(animeId: Long): List<Category>
fun getCategoriesByAnimeIdAsFlow(animeId: Long): Flow<List<Category>>
@Throws(DuplicateNameException::class)
suspend fun insert(name: String, order: Long)
@ -16,7 +20,5 @@ interface CategoryRepositoryAnime {
suspend fun delete(categoryId: Long)
suspend fun getCategoriesForAnime(animeId: Long): List<Category>
suspend fun checkDuplicateName(name: String): Boolean
}

View file

@ -0,0 +1,20 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class GetChapter(
private val chapterRepository: ChapterRepository,
) {
suspend fun await(id: Long): Chapter? {
return try {
chapterRepository.getChapterById(id)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
null
}
}
}

View file

@ -16,5 +16,7 @@ interface ChapterRepository {
suspend fun getChapterByMangaId(mangaId: Long): List<Chapter>
fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>>
suspend fun getChapterById(id: Long): Chapter?
suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>>
}

View file

@ -0,0 +1,20 @@
package eu.kanade.domain.episode.interactor
import eu.kanade.domain.episode.model.Episode
import eu.kanade.domain.episode.repository.EpisodeRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class GetEpisode(
private val episodeRepository: EpisodeRepository,
) {
suspend fun await(id: Long): Episode? {
return try {
episodeRepository.getEpisodeById(id)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
null
}
}
}

View file

@ -16,5 +16,7 @@ interface EpisodeRepository {
suspend fun getEpisodeByAnimeId(animeId: Long): List<Episode>
suspend fun getEpisodeById(id: Long): Episode?
fun getEpisodeByAnimeIdAsFlow(animeId: Long): Flow<List<Episode>>
}

View file

@ -20,6 +20,10 @@ class UpdateManga(
return mangaRepository.update(mangaUpdate)
}
suspend fun awaitAll(values: List<MangaUpdate>): Boolean {
return mangaRepository.updateAll(values)
}
suspend fun awaitUpdateFromSource(
localManga: Manga,
remoteManga: MangaInfo,

View file

@ -18,7 +18,9 @@ interface MangaRepository {
suspend fun resetViewerFlags(): Boolean
suspend fun moveMangaToCategories(mangaId: Long, categoryIds: List<Long>)
suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>)
suspend fun update(update: MangaUpdate): Boolean
suspend fun updateAll(values: List<MangaUpdate>): Boolean
}

View file

@ -19,7 +19,11 @@ class GetTracks(
}
}
suspend fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.subscribeTracksByMangaId(mangaId)
fun subscribe(): Flow<List<Track>> {
return trackRepository.getTracksAsFlow()
}
fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.getTracksByMangaIdAsFlow(mangaId)
}
}

View file

@ -7,7 +7,9 @@ interface TrackRepository {
suspend fun getTracksByMangaId(mangaId: Long): List<Track>
suspend fun subscribeTracksByMangaId(mangaId: Long): Flow<List<Track>>
fun getTracksAsFlow(): Flow<List<Track>>
fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>>
suspend fun delete(mangaId: Long, syncId: Long)

View file

@ -10,7 +10,6 @@ import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
@ -51,6 +50,14 @@ fun ChapterDownloadIndicator(
onClick(ChapterDownloadAction.START)
}
},
onLongClick = {
val chapterDownloadAction = when {
isDownloaded -> ChapterDownloadAction.DELETE
isDownloading -> ChapterDownloadAction.CANCEL
else -> ChapterDownloadAction.START_NOW
}
onClick(chapterDownloadAction)
},
) {
if (isDownloaded) {
Icon(

View file

@ -10,7 +10,6 @@ import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
@ -51,6 +50,14 @@ fun EpisodeDownloadIndicator(
onClick(EpisodeDownloadAction.START)
}
},
onLongClick = {
val episodeDownloadAction = when {
isDownloaded -> EpisodeDownloadAction.DELETE
isDownloading -> EpisodeDownloadAction.CANCEL
else -> EpisodeDownloadAction.START_NOW
}
onClick(episodeDownloadAction)
},
) {
if (isDownloaded) {
Icon(

View file

@ -0,0 +1,105 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.kanade.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.minimumTouchTargetSize
/**
* <a href="https://m3.material.io/components/icon-button/overview" class="external" target="_blank">Material Design standard icon button</a>.
*
* Icon buttons help people take supplementary actions with a single tap. Theyre used when a
* compact button is required, such as in a toolbar or image list.
*
* ![Standard icon button image](https://developer.android.com/images/reference/androidx/compose/material3/standard-icon-button.png)
*
* [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
* custom icon, note that the typical size for the internal icon is 24 x 24 dp.
* This icon button has an overall minimum touch target size of 48 x 48dp, to meet accessibility
* guidelines.
*
* @sample androidx.compose.material3.samples.IconButtonSample
*
* Tachiyomi changes:
* * Add on long click
*
* @param onClick called when this icon button is clicked
* @param modifier the [Modifier] to be applied to this icon button
* @param enabled controls the enabled state of this icon button. When `false`, this component will
* not respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this icon button. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this icon button in different states.
* @param colors [IconButtonColors] that will be used to resolve the colors used for this icon
* button in different states. See [IconButtonDefaults.iconButtonColors].
* @param content the content of this icon button, typically an [Icon]
*/
@Composable
fun IconButton(
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
content: @Composable () -> Unit,
) {
Box(
modifier =
modifier
.minimumTouchTargetSize()
.size(IconButtonTokens.StateLayerSize)
.background(color = colors.containerColor(enabled).value)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
),
contentAlignment = Alignment.Center,
) {
val contentColor = colors.contentColor(enabled).value
CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
}
}
object IconButtonTokens {
val StateLayerSize = 40.0.dp
}

View file

@ -3,20 +3,15 @@ package eu.kanade.tachiyomi.data.database
import androidx.sqlite.db.SupportSQLiteOpenHelper
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
import eu.kanade.tachiyomi.data.database.mappers.AnimeCategoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.AnimeHistoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.AnimeTrackTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.AnimeTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.EpisodeTypeMapping
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.AnimeCategory
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
import eu.kanade.tachiyomi.data.database.models.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.database.queries.AnimeCategoryQueries
import eu.kanade.tachiyomi.data.database.queries.AnimeQueries
import eu.kanade.tachiyomi.data.database.queries.AnimeTrackQueries
import eu.kanade.tachiyomi.data.database.queries.CategoryQueries
import eu.kanade.tachiyomi.data.database.queries.EpisodeQueries
@ -26,17 +21,13 @@ import eu.kanade.tachiyomi.data.database.queries.EpisodeQueries
class AnimeDatabaseHelper(
openHelper: SupportSQLiteOpenHelper,
) :
AnimeQueries, EpisodeQueries, AnimeTrackQueries, CategoryQueries, AnimeCategoryQueries {
AnimeQueries, EpisodeQueries, CategoryQueries, AnimeCategoryQueries {
override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(openHelper)
.addTypeMapping(Anime::class.java, AnimeTypeMapping())
.addTypeMapping(Episode::class.java, EpisodeTypeMapping())
.addTypeMapping(AnimeTrack::class.java, AnimeTrackTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(AnimeCategory::class.java, AnimeCategoryTypeMapping())
.addTypeMapping(AnimeHistory::class.java, AnimeHistoryTypeMapping())
.build()
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
}

View file

@ -4,21 +4,16 @@ import androidx.sqlite.db.SupportSQLiteOpenHelper
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.ChapterTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.HistoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.MangaCategoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.MangaTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.TrackTypeMapping
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.queries.CategoryQueries
import eu.kanade.tachiyomi.data.database.queries.ChapterQueries
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
/**
* This class provides operations to manage the database through its interfaces.
@ -26,17 +21,13 @@ import eu.kanade.tachiyomi.data.database.queries.TrackQueries
class DatabaseHelper(
openHelper: SupportSQLiteOpenHelper,
) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries {
MangaQueries, ChapterQueries, CategoryQueries, MangaCategoryQueries {
override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(openHelper)
.addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(Track::class.java, TrackTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping())
.build()
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
}

View file

@ -1,61 +0,0 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
import eu.kanade.tachiyomi.data.database.models.AnimeHistoryImpl
import eu.kanade.tachiyomi.data.database.tables.AnimeHistoryTable.COL_EPISODE_ID
import eu.kanade.tachiyomi.data.database.tables.AnimeHistoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.AnimeHistoryTable.COL_LAST_SEEN
import eu.kanade.tachiyomi.data.database.tables.AnimeHistoryTable.TABLE
class AnimeHistoryTypeMapping : SQLiteTypeMapping<AnimeHistory>(
AnimeHistoryPutResolver(),
AnimeHistoryGetResolver(),
AnimeHistoryDeleteResolver(),
)
open class AnimeHistoryPutResolver : DefaultPutResolver<AnimeHistory>() {
override fun mapToInsertQuery(obj: AnimeHistory) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: AnimeHistory) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: AnimeHistory) =
contentValuesOf(
COL_ID to obj.id,
COL_EPISODE_ID to obj.episode_id,
COL_LAST_SEEN to obj.last_seen,
)
}
class AnimeHistoryGetResolver : DefaultGetResolver<AnimeHistory>() {
override fun mapFromCursor(cursor: Cursor): AnimeHistory = AnimeHistoryImpl().apply {
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
episode_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_EPISODE_ID))
last_seen = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_SEEN))
}
}
class AnimeHistoryDeleteResolver : DefaultDeleteResolver<AnimeHistory>() {
override fun mapToDeleteQuery(obj: AnimeHistory) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View file

@ -1,91 +0,0 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.AnimeTrackImpl
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_ANIME_ID
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_FINISH_DATE
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_LAST_EPISODE_SEEN
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_LIBRARY_ID
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_MEDIA_ID
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_SCORE
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_START_DATE
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_SYNC_ID
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_TOTAL_EPISODES
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.COL_TRACKING_URL
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable.TABLE
class AnimeTrackTypeMapping : SQLiteTypeMapping<AnimeTrack>(
AnimeTrackPutResolver(),
AnimeTrackGetResolver(),
AnimeTrackDeleteResolver(),
)
class AnimeTrackPutResolver : DefaultPutResolver<AnimeTrack>() {
override fun mapToInsertQuery(obj: AnimeTrack) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: AnimeTrack) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: AnimeTrack) =
contentValuesOf(
COL_ID to obj.id,
COL_ANIME_ID to obj.anime_id,
COL_SYNC_ID to obj.sync_id,
COL_MEDIA_ID to obj.media_id,
COL_LIBRARY_ID to obj.library_id,
COL_TITLE to obj.title,
COL_LAST_EPISODE_SEEN to obj.last_episode_seen,
COL_TOTAL_EPISODES to obj.total_episodes,
COL_STATUS to obj.status,
COL_TRACKING_URL to obj.tracking_url,
COL_SCORE to obj.score,
COL_START_DATE to obj.started_watching_date,
COL_FINISH_DATE to obj.finished_watching_date,
)
}
class AnimeTrackGetResolver : DefaultGetResolver<AnimeTrack>() {
override fun mapFromCursor(cursor: Cursor): AnimeTrack = AnimeTrackImpl().apply {
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
anime_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ANIME_ID))
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
media_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
last_episode_seen = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_EPISODE_SEEN))
total_episodes = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_EPISODES))
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
started_watching_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
finished_watching_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
}
}
class AnimeTrackDeleteResolver : DefaultDeleteResolver<AnimeTrack>() {
override fun mapToDeleteQuery(obj: AnimeTrack) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View file

@ -1,64 +0,0 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
class HistoryTypeMapping : SQLiteTypeMapping<History>(
HistoryPutResolver(),
HistoryGetResolver(),
HistoryDeleteResolver(),
)
open class HistoryPutResolver : DefaultPutResolver<History>() {
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: History) =
contentValuesOf(
COL_ID to obj.id,
COL_CHAPTER_ID to obj.chapter_id,
COL_LAST_READ to obj.last_read,
COL_TIME_READ to obj.time_read,
)
}
class HistoryGetResolver : DefaultGetResolver<History>() {
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
chapter_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_CHAPTER_ID))
last_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_READ))
time_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_TIME_READ))
}
}
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View file

@ -1,91 +0,0 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_FINISH_DATE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_START_DATE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
class TrackTypeMapping : SQLiteTypeMapping<Track>(
TrackPutResolver(),
TrackGetResolver(),
TrackDeleteResolver(),
)
class TrackPutResolver : DefaultPutResolver<Track>() {
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Track) =
contentValuesOf(
COL_ID to obj.id,
COL_MANGA_ID to obj.manga_id,
COL_SYNC_ID to obj.sync_id,
COL_MEDIA_ID to obj.media_id,
COL_LIBRARY_ID to obj.library_id,
COL_TITLE to obj.title,
COL_LAST_CHAPTER_READ to obj.last_chapter_read,
COL_TOTAL_CHAPTERS to obj.total_chapters,
COL_STATUS to obj.status,
COL_TRACKING_URL to obj.tracking_url,
COL_SCORE to obj.score,
COL_START_DATE to obj.started_reading_date,
COL_FINISH_DATE to obj.finished_reading_date,
)
}
class TrackGetResolver : DefaultGetResolver<Track>() {
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
media_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ))
total_chapters = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_CHAPTERS))
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
started_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
finished_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
}
}
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View file

@ -1,7 +1,5 @@
package eu.kanade.tachiyomi.data.database.models
import dataanime.GetCategories
class AnimeCategory {
var id: Long? = null
@ -18,12 +16,5 @@ class AnimeCategory {
ac.category_id = category.id!!
return ac
}
fun create(anime: Anime, category: GetCategories): AnimeCategory {
val ac = AnimeCategory()
ac.anime_id = anime.id!!
ac.category_id = category.id.toInt()
return ac
}
}
}

View file

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.data.database.models
class AnimeEpisode(val anime: Anime, val episode: Episode)

View file

@ -1,37 +0,0 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
/**
* Object containing the history statistics of a chapter
*/
interface AnimeHistory : Serializable {
/**
* Id of history object.
*/
var id: Long?
/**
* Chapter id of history object.
*/
var episode_id: Long
/**
* Last time episode was read in time long format
*/
var last_seen: Long
companion object {
/**
* History constructor
*
* @param chapter chapter object
* @return history object
*/
fun create(episode: Episode): AnimeHistory = AnimeHistoryImpl().apply {
this.episode_id = episode.id!!
}
}
}

View file

@ -1,22 +0,0 @@
package eu.kanade.tachiyomi.data.database.models
/**
* Object containing the history statistics of a chapter
*/
class AnimeHistoryImpl : AnimeHistory {
/**
* Id of history object.
*/
override var id: Long? = null
/**
* Chapter id of history object.
*/
override var episode_id: Long = 0
/**
* Last time chapter was read in time long format
*/
override var last_seen: Long = 0
}

View file

@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
/**
* Object containing the history statistics of a chapter
*/
interface History : Serializable {
/**
* Id of history object.
*/
var id: Long?
/**
* Chapter id of history object.
*/
var chapter_id: Long
/**
* Last time chapter was read in time long format
*/
var last_read: Long
/**
* Total time chapter was read
*/
var time_read: Long
companion object {
/**
* History constructor
*
* @param chapter chapter object
* @return history object
*/
fun create(chapter: Chapter): History = HistoryImpl().apply {
this.chapter_id = chapter.id!!
}
}
}

View file

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.data.database.models
/**
* Object containing the history statistics of a chapter
*/
class HistoryImpl : History {
/**
* Id of history object.
*/
override var id: Long? = null
/**
* Chapter id of history object.
*/
override var chapter_id: Long = 0
/**
* Last time chapter was read in time long format
*/
override var last_read: Long = 0
/**
* Total time chapter was read
*/
override var time_read: Long = 0
}

View file

@ -1,7 +1,5 @@
package eu.kanade.tachiyomi.data.database.models
import data.GetCategories
class MangaCategory {
var id: Long? = null
@ -18,12 +16,5 @@ class MangaCategory {
mc.category_id = category.id!!
return mc
}
fun create(manga: Manga, category: GetCategories): MangaCategory {
val mc = MangaCategory()
mc.manga_id = manga.id!!
mc.category_id = category.id.toInt()
return mc
}
}
}

View file

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.data.database.models
class MangaChapter(val manga: Manga, val chapter: Chapter)

View file

@ -60,8 +60,6 @@ interface AnimeQueries : DbProvider {
fun insertAnime(anime: Anime) = db.put().`object`(anime).prepare()
fun insertAnimes(animes: List<Anime>) = db.put().objects(animes).prepare()
fun updateEpisodeFlags(anime: Anime) = db.put()
.`object`(anime)
.withPutResolver(AnimeFlagsPutResolver(AnimeTable.COL_EPISODE_FLAGS, Anime::episode_flags))
@ -76,34 +74,4 @@ interface AnimeQueries : DbProvider {
.`object`(anime)
.withPutResolver(AnimeFlagsPutResolver(AnimeTable.COL_VIEWER, Anime::viewer_flags))
.prepare()
fun getLastSeenAnime() = db.get()
.listOfObjects(Anime::class.java)
.withQuery(
RawQuery.builder()
.query(getLastSeenAnimeQuery())
.observesTables(AnimeTable.TABLE)
.build(),
)
.prepare()
fun getLatestEpisodeAnime() = db.get()
.listOfObjects(Anime::class.java)
.withQuery(
RawQuery.builder()
.query(getLatestEpisodeAnimeQuery())
.observesTables(AnimeTable.TABLE)
.build(),
)
.prepare()
fun getEpisodeFetchDateAnime() = db.get()
.listOfObjects(Anime::class.java)
.withQuery(
RawQuery.builder()
.query(getEpisodeFetchDateAnimeQuery())
.observesTables(AnimeTable.TABLE)
.build(),
)
.prepare()
}

View file

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.AnimeTrack
import eu.kanade.tachiyomi.data.database.tables.AnimeTrackTable
interface AnimeTrackQueries : DbProvider {
fun getTracks() = db.get()
.listOfObjects(AnimeTrack::class.java)
.withQuery(
Query.builder()
.table(AnimeTrackTable.TABLE)
.build(),
)
.prepare()
}

View file

@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
interface CategoryQueries : DbProvider {
@ -20,25 +18,23 @@ interface CategoryQueries : DbProvider {
)
.prepare()
fun getCategoriesForManga(manga: Manga) = db.get()
fun getCategoriesForManga(mangaId: Long) = db.get()
.listOfObjects(Category::class.java)
.withQuery(
RawQuery.builder()
.query(getCategoriesForMangaQuery())
.args(manga.id)
.args(mangaId)
.build(),
)
.prepare()
fun getCategoriesForAnime(anime: Anime) = db.get()
fun getCategoriesForAnime(animeId: Long) = db.get()
.listOfObjects(Category::class.java)
.withQuery(
RawQuery.builder()
.query(getCategoriesForAnimeQuery())
.args(anime.id)
.args(animeId)
.build(),
)
.prepare()
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
}

View file

@ -3,19 +3,18 @@ package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
interface ChapterQueries : DbProvider {
fun getChapters(manga: Manga) = db.get()
fun getChapters(mangaId: Long) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.whereArgs(mangaId)
.build(),
)
.prepare()
@ -46,9 +45,4 @@ interface ChapterQueries : DbProvider {
.`object`(chapter)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
}

View file

@ -2,20 +2,19 @@ package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.database.resolvers.EpisodeProgressPutResolver
import eu.kanade.tachiyomi.data.database.tables.EpisodeTable
interface EpisodeQueries : DbProvider {
fun getEpisodes(anime: Anime) = db.get()
fun getEpisodes(animeId: Long) = db.get()
.listOfObjects(Episode::class.java)
.withQuery(
Query.builder()
.table(EpisodeTable.TABLE)
.where("${EpisodeTable.COL_ANIME_ID} = ?")
.whereArgs(anime.id)
.whereArgs(animeId)
.build(),
)
.prepare()
@ -57,9 +56,4 @@ interface EpisodeQueries : DbProvider {
.`object`(episode)
.withPutResolver(EpisodeProgressPutResolver())
.prepare()
fun updateEpisodesProgress(episodes: List<Episode>) = db.put()
.objects(episodes)
.withPutResolver(EpisodeProgressPutResolver())
.prepare()
}

View file

@ -60,8 +60,6 @@ interface MangaQueries : DbProvider {
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
fun updateChapterFlags(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
@ -76,34 +74,4 @@ interface MangaQueries : DbProvider {
.`object`(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
.prepare()
fun getLastReadManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
)
.prepare()
fun getLatestChapterManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getLatestChapterMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
)
.prepare()
fun getChapterFetchDateManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getChapterFetchDateMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
)
.prepare()
}

View file

@ -1,12 +1,10 @@
package eu.kanade.tachiyomi.data.database.queries
import eu.kanade.tachiyomi.data.database.tables.AnimeCategoryTable as AnimeCategory
import eu.kanade.tachiyomi.data.database.tables.AnimeHistoryTable as AnimeHistory
import eu.kanade.tachiyomi.data.database.tables.AnimeTable as Anime
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
import eu.kanade.tachiyomi.data.database.tables.EpisodeTable as Episode
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
@ -71,72 +69,6 @@ val animelibQuery =
ON MC.${AnimeCategory.COL_ANIME_ID} = M.${Anime.COL_ID}
"""
fun getLastReadMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
JOIN ${History.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER BY max DESC
"""
fun getLastSeenAnimeQuery() =
"""
SELECT ${Anime.TABLE}.*, MAX(${AnimeHistory.TABLE}.${AnimeHistory.COL_LAST_SEEN}) AS max
FROM ${Anime.TABLE}
JOIN ${Episode.TABLE}
ON ${Anime.TABLE}.${Anime.COL_ID} = ${Episode.TABLE}.${Episode.COL_ANIME_ID}
JOIN ${AnimeHistory.TABLE}
ON ${Episode.TABLE}.${Episode.COL_ID} = ${AnimeHistory.TABLE}.${AnimeHistory.COL_EPISODE_ID}
WHERE ${Anime.TABLE}.${Anime.COL_FAVORITE} = 1
GROUP BY ${Anime.TABLE}.${Anime.COL_ID}
ORDER BY max DESC
"""
fun getLatestChapterMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER by max DESC
"""
fun getLatestEpisodeAnimeQuery() =
"""
SELECT ${Anime.TABLE}.*, MAX(${Episode.TABLE}.${Episode.COL_DATE_UPLOAD}) AS max
FROM ${Anime.TABLE}
JOIN ${Episode.TABLE}
ON ${Anime.TABLE}.${Anime.COL_ID} = ${Episode.TABLE}.${Episode.COL_ANIME_ID}
GROUP BY ${Anime.TABLE}.${Anime.COL_ID}
ORDER by max DESC
"""
fun getChapterFetchDateMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER by max DESC
"""
fun getEpisodeFetchDateAnimeQuery() =
"""
SELECT ${Anime.TABLE}.*, MAX(${Episode.TABLE}.${Episode.COL_DATE_FETCH}) AS max
FROM ${Anime.TABLE}
JOIN ${Episode.TABLE}
ON ${Anime.TABLE}.${Anime.COL_ID} = ${Episode.TABLE}.${Episode.COL_ANIME_ID}
GROUP BY ${Anime.TABLE}.${Anime.COL_ID}
ORDER by max DESC
"""
/**
* Query to get the categories for a manga.
*/

View file

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.tables.TrackTable
interface TrackQueries : DbProvider {
fun getTracks() = db.get()
.listOfObjects(Track::class.java)
.withQuery(
Query.builder()
.table(TrackTable.TABLE)
.build(),
)
.prepare()
}

View file

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.mappers.AnimeGetResolver
import eu.kanade.tachiyomi.data.database.mappers.EpisodeGetResolver
import eu.kanade.tachiyomi.data.database.models.AnimeEpisode
class AnimeEpisodeGetResolver : DefaultGetResolver<AnimeEpisode>() {
companion object {
val INSTANCE = AnimeEpisodeGetResolver()
}
private val animeGetResolver = AnimeGetResolver()
private val episodeGetResolver = EpisodeGetResolver()
override fun mapFromCursor(cursor: Cursor): AnimeEpisode {
val anime = animeGetResolver.mapFromCursor(cursor)
val episode = episodeGetResolver.mapFromCursor(cursor)
anime.id = episode.anime_id
anime.url = cursor.getString(cursor.getColumnIndexOrThrow("animeUrl"))
return AnimeEpisode(anime, episode)
}
}

View file

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.MangaChapter
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
companion object {
val INSTANCE = MangaChapterGetResolver()
}
private val mangaGetResolver = MangaGetResolver()
private val chapterGetResolver = ChapterGetResolver()
override fun mapFromCursor(cursor: Cursor): MangaChapter {
val manga = mangaGetResolver.mapFromCursor(cursor)
val chapter = chapterGetResolver.mapFromCursor(cursor)
manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
return MangaChapter(manga, chapter)
}
}

View file

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables
object AnimeHistoryTable {
/**
* Table name
*/
const val TABLE = "animehistory"
/**
* Id column name
*/
const val COL_ID = "_id"
/**
* Episode id column name
*/
const val COL_EPISODE_ID = "episode_id"
/**
* Last seen column name
*/
const val COL_LAST_SEEN = "last_seen"
}

View file

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables
object AnimeTrackTable {
const val TABLE = "anime_sync"
const val COL_ID = "_id"
const val COL_ANIME_ID = "anime_id"
const val COL_SYNC_ID = "sync_id"
const val COL_MEDIA_ID = "remote_id"
const val COL_LIBRARY_ID = "library_id"
const val COL_TITLE = "title"
const val COL_LAST_EPISODE_SEEN = "last_episode_seen"
const val COL_STATUS = "status"
const val COL_SCORE = "score"
const val COL_TOTAL_EPISODES = "total_episodes"
const val COL_TRACKING_URL = "remote_url"
const val COL_START_DATE = "start_date"
const val COL_FINISH_DATE = "finish_date"
val insertFromTempTable: String
get() =
"""
|INSERT INTO $TABLE($COL_ID,$COL_ANIME_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_EPISODE_SEEN,$COL_TOTAL_EPISODES,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|SELECT $COL_ID,$COL_ANIME_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_EPISODE_SEEN,$COL_TOTAL_EPISODES,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|FROM ${TABLE}_tmp
""".trimMargin()
}

View file

@ -1,29 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables
object HistoryTable {
/**
* Table name
*/
const val TABLE = "history"
/**
* Id column name
*/
const val COL_ID = "_id"
/**
* Chapter id column name
*/
const val COL_CHAPTER_ID = "chapter_id"
/**
* Last read column name
*/
const val COL_LAST_READ = "last_read"
/**
* Time read column name
*/
const val COL_TIME_READ = "time_read"
}

View file

@ -1,32 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables
object TrackTable {
const val TABLE = "manga_sync"
const val COL_ID = "_id"
const val COL_MANGA_ID = "manga_id"
const val COL_SYNC_ID = "sync_id"
const val COL_MEDIA_ID = "remote_id"
const val COL_LIBRARY_ID = "library_id"
const val COL_TITLE = "title"
const val COL_LAST_CHAPTER_READ = "last_chapter_read"
const val COL_STATUS = "status"
const val COL_SCORE = "score"
const val COL_TOTAL_CHAPTERS = "total_chapters"
const val COL_TRACKING_URL = "remote_url"
const val COL_START_DATE = "start_date"
const val COL_FINISH_DATE = "finish_date"
}

View file

@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.download.model.AnimeDownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import rx.Observable
import uy.kohesive.injekt.Injekt
@ -104,10 +105,12 @@ class AnimeDownloadManager(
fun startDownloadNow(episodeId: Long?) {
if (episodeId == null) return
val download = downloader.queue.find { it.episode.id == episodeId } ?: return
val download = downloader.queue.find { it.episode.id == episodeId }
// If not in queue try to start a new download
val toAdd = download ?: runBlocking { AnimeDownload.fromEpisodeId(episodeId) } ?: return
val queue = downloader.queue.toMutableList()
queue.remove(download)
queue.add(0, download)
download?.let { queue.remove(it) }
queue.add(0, toAdd)
reorderQueue(queue)
if (isPaused()) {
if (AnimeDownloadService.isRunning(context)) {
@ -363,7 +366,7 @@ class AnimeDownloadManager(
private fun getEpisodesToDelete(episodes: List<Episode>, anime: Anime): List<Episode> {
// Retrieve the categories that are set to exclude from being deleted on read
val categoriesToExclude = preferences.removeExcludeAnimeCategories().get().map(String::toInt)
val categoriesForAnime = db.getCategoriesForAnime(anime).executeAsBlocking()
val categoriesForAnime = db.getCategoriesForAnime(anime.id!!).executeAsBlocking()
.mapNotNull { it.id }
.takeUnless { it.isEmpty() }
?: listOf(0)

View file

@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import rx.Observable
import uy.kohesive.injekt.Injekt
@ -104,10 +105,12 @@ class DownloadManager(
fun startDownloadNow(chapterId: Long?) {
if (chapterId == null) return
val download = downloader.queue.find { it.chapter.id == chapterId } ?: return
val download = downloader.queue.find { it.chapter.id == chapterId }
// If not in queue try to start a new download
val toAdd = download ?: runBlocking { Download.fromChapterId(chapterId) } ?: return
val queue = downloader.queue.toMutableList()
queue.remove(download)
queue.add(0, download)
download?.let { queue.remove(it) }
queue.add(0, toAdd)
reorderQueue(queue)
if (isPaused()) {
if (DownloadService.isRunning(context)) {
@ -359,7 +362,7 @@ class DownloadManager(
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
// Retrieve the categories that are set to exclude from being deleted on read
val categoriesToExclude = preferences.removeExcludeCategories().get().map(String::toInt)
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
val categoriesForManga = db.getCategoriesForManga(manga.id!!).executeAsBlocking()
.mapNotNull { it.id }
.takeUnless { it.isEmpty() }
?: listOf(0)

View file

@ -493,7 +493,12 @@ class Downloader(
// check if the original page was previously splitted before then skip.
if (imageFile.name!!.contains("__")) return true
return ImageUtil.splitTallImage(imageFile, imageFilePath)
return try {
ImageUtil.splitTallImage(imageFile, imageFilePath)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
/**

View file

@ -1,10 +1,17 @@
package eu.kanade.tachiyomi.data.download.model
import eu.kanade.domain.anime.interactor.GetAnimeById
import eu.kanade.domain.anime.model.toDbAnime
import eu.kanade.domain.episode.interactor.GetEpisode
import eu.kanade.domain.episode.model.toDbEpisode
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.Episode
import rx.subjects.PublishSubject
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
data class AnimeDownload(
val source: AnimeHttpSource,
@ -77,4 +84,19 @@ data class AnimeDownload(
DOWNLOADED(3),
ERROR(4),
}
companion object {
suspend fun fromEpisodeId(
chapterId: Long,
getEpisode: GetEpisode = Injekt.get(),
getAnimeById: GetAnimeById = Injekt.get(),
sourceManager: AnimeSourceManager = Injekt.get(),
): AnimeDownload? {
val episode = getEpisode.await(chapterId) ?: return null
val anime = getAnimeById.await(episode.animeId) ?: return null
val source = sourceManager.get(anime.source) as? AnimeHttpSource ?: return null
return AnimeDownload(source, anime.toDbAnime(), episode.toDbEpisode())
}
}
}

View file

@ -1,10 +1,17 @@
package eu.kanade.tachiyomi.data.download.model
import eu.kanade.domain.chapter.interactor.GetChapter
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.interactor.GetMangaById
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import rx.subjects.PublishSubject
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
data class Download(
val source: HttpSource,
@ -57,4 +64,19 @@ data class Download(
DOWNLOADED(3),
ERROR(4),
}
companion object {
suspend fun fromChapterId(
chapterId: Long,
getChapter: GetChapter = Injekt.get(),
getMangaById: GetMangaById = Injekt.get(),
sourceManager: SourceManager = Injekt.get(),
): Download? {
val chapter = getChapter.await(chapterId) ?: return null
val manga = getMangaById.await(chapter.mangaId) ?: return null
val source = sourceManager.get(manga.source) as? HttpSource ?: return null
return Download(source, manga.toDbManga(), chapter.toDbChapter())
}
}
}

View file

@ -16,7 +16,7 @@ import eu.kanade.domain.animetrack.interactor.InsertAnimeTrack
import eu.kanade.domain.animetrack.model.toDbTrack
import eu.kanade.domain.animetrack.model.toDomainTrack
import eu.kanade.domain.category.interactor.GetCategoriesAnime
import eu.kanade.domain.category.interactor.MoveAnimeToCategories
import eu.kanade.domain.category.interactor.SetAnimeCategories
import eu.kanade.domain.episode.interactor.SyncEpisodesWithSource
import eu.kanade.domain.episode.interactor.SyncEpisodesWithTrackServiceTwoWay
import eu.kanade.domain.episode.interactor.UpdateEpisode
@ -91,7 +91,7 @@ class AnimePresenter(
private val getCategories: GetCategoriesAnime = Injekt.get(),
private val deleteTrack: DeleteAnimeTrack = Injekt.get(),
private val getTracks: GetAnimeTracks = Injekt.get(),
private val moveAnimeToCategories: MoveAnimeToCategories = Injekt.get(),
private val moveAnimeToCategories: SetAnimeCategories = Injekt.get(),
private val insertTrack: InsertAnimeTrack = Injekt.get(),
private val syncEpisodesWithTrackServiceTwoWay: SyncEpisodesWithTrackServiceTwoWay = Injekt.get(),
) : BasePresenter<AnimeController>() {

View file

@ -29,6 +29,8 @@ import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.animesource.globalsearch.GlobalAnimeSearchController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser
@ -36,6 +38,7 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
import eu.kanade.tachiyomi.widget.EmptyView
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -226,6 +229,7 @@ class AnimelibController(
destroyActionModeIfNeeded()
adapter?.onDestroy()
adapter = null
settingsSheet?.sheetScope?.cancel()
settingsSheet = null
tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null
@ -541,25 +545,29 @@ class AnimelibController(
* Move the selected anime to a list of categories.
*/
private fun showAnimeCategoriesDialog() {
// Create a copy of selected anime
val animes = selectedAnimes.toList()
viewScope.launchIO {
// Create a copy of selected anime
val animes = selectedAnimes.toList()
// Hide the default category because it has a different behavior than the ones from db.
val categories = presenter.categories.filter { it.id != 0 }
// Hide the default category because it has a different behavior than the ones from db.
val categories = presenter.categories.filter { it.id != 0 }
// Get indexes of the common categories to preselect.
val common = presenter.getCommonCategories(animes)
// Get indexes of the mix categories to preselect.
val mix = presenter.getMixCategories(animes)
val preselected = categories.map {
when (it) {
in common -> QuadStateTextView.State.CHECKED.ordinal
in mix -> QuadStateTextView.State.INDETERMINATE.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
// Get indexes of the common categories to preselect.
val common = presenter.getCommonCategories(animes)
// Get indexes of the mix categories to preselect.
val mix = presenter.getMixCategories(animes)
val preselected = categories.map {
when (it) {
in common -> QuadStateTextView.State.CHECKED.ordinal
in mix -> QuadStateTextView.State.INDETERMINATE.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
}
}.toIntArray()
launchUI {
ChangeAnimeCategoriesDialog(this@AnimelibController, animes, categories, preselected)
.showDialog(router)
}
}.toIntArray()
ChangeAnimeCategoriesDialog(this, animes, categories, preselected)
.showDialog(router)
}
}
private fun downloadUnseenEpisodes() {
@ -579,7 +587,7 @@ class AnimelibController(
}
override fun updateCategoriesForAnimes(animes: List<Anime>, addCategories: List<Category>, removeCategories: List<Category>) {
presenter.updateAnimesToCategories(animes, addCategories, removeCategories)
presenter.setAnimeCategories(animes, addCategories, removeCategories)
destroyActionModeIfNeeded()
}

View file

@ -2,13 +2,24 @@ package eu.kanade.tachiyomi.ui.animelib
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.core.util.asObservable
import eu.kanade.data.AnimeDatabaseHandler
import eu.kanade.domain.anime.interactor.UpdateAnime
import eu.kanade.domain.anime.model.AnimeUpdate
import eu.kanade.domain.animetrack.interactor.GetAnimeTracks
import eu.kanade.domain.category.interactor.GetCategoriesAnime
import eu.kanade.domain.category.interactor.SetAnimeCategories
import eu.kanade.domain.category.model.toDbCategory
import eu.kanade.domain.episode.interactor.GetEpisodeByAnimeId
import eu.kanade.domain.episode.interactor.UpdateEpisode
import eu.kanade.domain.episode.model.EpisodeUpdate
import eu.kanade.domain.episode.model.toDbEpisode
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.AnimeCategory
import eu.kanade.tachiyomi.data.database.models.AnimelibAnime
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
@ -23,6 +34,8 @@ import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -47,7 +60,13 @@ private typealias AnimelibMap = Map<Int, List<AnimelibItem>>
* Presenter of [AnimelibController].
*/
class AnimelibPresenter(
private val db: AnimeDatabaseHelper = Injekt.get(),
private val handler: AnimeDatabaseHandler = Injekt.get(),
private val getTracks: GetAnimeTracks = Injekt.get(),
private val getCategories: GetCategoriesAnime = Injekt.get(),
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
private val updateEpisode: UpdateEpisode = Injekt.get(),
private val updateAnime: UpdateAnime = Injekt.get(),
private val setAnimeCategories: SetAnimeCategories = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(),
private val coverCache: AnimeCoverCache = Injekt.get(),
private val sourceManager: AnimeSourceManager = Injekt.get(),
@ -92,6 +111,7 @@ class AnimelibPresenter(
* Subscribes to animelib if needed.
*/
fun subscribeAnimelib() {
// TODO: Move this to a coroutine world
if (animelibSubscription.isNullOrUnsubscribed()) {
animelibSubscription = getAnimelibObservable()
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
@ -115,7 +135,7 @@ class AnimelibPresenter(
*
* @param map the map to filter.
*/
private fun applyFilters(map: AnimelibMap, trackMap: Map<Long, Map<Int, Boolean>>): AnimelibMap {
private fun applyFilters(map: AnimelibMap, trackMap: Map<Long, Map<Long, Boolean>>): AnimelibMap {
val downloadedOnly = preferences.downloadedOnly().get()
val filterDownloaded = preferences.filterDownloaded().get()
val filterUnseen = preferences.filterUnread().get()
@ -251,20 +271,32 @@ class AnimelibPresenter(
* @param map the map to sort.
*/
private fun applySort(categories: List<Category>, map: AnimelibMap): AnimelibMap {
val lastReadAnime by lazy {
val lastSeenAnime by lazy {
var counter = 0
// Result comes as newest to oldest so it's reversed
db.getLastSeenAnime().executeAsBlocking().reversed().associate { it.id!! to counter++ }
// TODO: Make [applySort] a suspended function
runBlocking {
handler.awaitList {
animesQueries.getLastSeen()
}.associate { it._id to counter++ }
}
}
val latestChapterAnime by lazy {
val latestEpisodeAnime by lazy {
var counter = 0
// Result comes as newest to oldest so it's reversed
db.getLatestEpisodeAnime().executeAsBlocking().reversed().associate { it.id!! to counter++ }
// TODO: Make [applySort] a suspended function
runBlocking {
handler.awaitList {
animesQueries.getLatestByEpisodeUploadDate()
}.associate { it._id to counter++ }
}
}
val chapterFetchDateAnime by lazy {
var counter = 0
// Result comes as newest to oldest so it's reversed
db.getEpisodeFetchDateAnime().executeAsBlocking().reversed().associate { it.id!! to counter++ }
// TODO: Make [applySort] a suspended function
runBlocking {
handler.awaitList {
animesQueries.getLatestByEpisodeFetchDate()
}.associate { it._id to counter++ }
}
}
val sortingModes = categories.associate { category ->
@ -287,8 +319,8 @@ class AnimelibPresenter(
collator.compare(i1.anime.title.lowercase(locale), i2.anime.title.lowercase(locale))
}
SortModeSetting.LAST_READ -> {
val anime1LastRead = lastReadAnime[i1.anime.id!!] ?: 0
val anime2LastRead = lastReadAnime[i2.anime.id!!] ?: 0
val anime1LastRead = lastSeenAnime[i1.anime.id!!] ?: 0
val anime2LastRead = lastSeenAnime[i2.anime.id!!] ?: 0
anime1LastRead.compareTo(anime2LastRead)
}
SortModeSetting.LAST_MANGA_UPDATE -> {
@ -305,10 +337,10 @@ class AnimelibPresenter(
i1.anime.totalEpisodes.compareTo(i2.anime.totalEpisodes)
}
SortModeSetting.LATEST_CHAPTER -> {
val anime1latestEpisode = latestChapterAnime[i1.anime.id!!]
?: latestChapterAnime.size
val anime2latestEpisode = latestChapterAnime[i2.anime.id!!]
?: latestChapterAnime.size
val anime1latestEpisode = latestEpisodeAnime[i1.anime.id!!]
?: latestEpisodeAnime.size
val anime2latestEpisode = latestEpisodeAnime[i2.anime.id!!]
?: latestEpisodeAnime.size
anime1latestEpisode.compareTo(anime2latestEpisode)
}
SortModeSetting.CHAPTER_FETCH_DATE -> {
@ -367,7 +399,7 @@ class AnimelibPresenter(
* @return an observable of the categories.
*/
private fun getCategoriesObservable(): Observable<List<Category>> {
return db.getCategories().asRxObservable()
return getCategories.subscribe().map { it.map { it.toDbCategory() } }.asObservable()
}
/**
@ -379,7 +411,36 @@ class AnimelibPresenter(
private fun getAnimelibAnimesObservable(): Observable<AnimelibMap> {
val defaultLibraryDisplayMode = preferences.libraryDisplayMode()
val shouldSetFromCategory = preferences.categorizedDisplaySettings()
return db.getAnimelibAnimes().asRxObservable()
// TODO: Move this to domain/data layer
return handler
.subscribeToList {
animesQueries.getAnimelib { _id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List<String>?, title: String, status: Long, thumbnail_url: String?, favorite: Boolean, last_update: Long?, next_update: Long?, initialized: Boolean, viewer: Long, episode_flags: Long, cover_last_modified: Long, date_added: Long, unseen_count: Long, seen_count: Long, category: Long ->
AnimelibAnime().apply {
this.id = _id
this.source = source
this.url = url
this.artist = artist
this.author = author
this.description = description
this.genre = genre?.joinToString()
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnail_url
this.favorite = favorite
this.last_update = last_update ?: 0
this.initialized = initialized
this.viewer_flags = viewer.toInt()
this.episode_flags = episode_flags.toInt()
this.cover_last_modified = cover_last_modified
this.date_added = date_added
this.unseenCount = unseen_count.toInt()
this.seenCount = seen_count.toInt()
this.category = category.toInt()
}
}
}
.asObservable()
.map { list ->
list.map { animelibAnime ->
// Display mode based on user preference: take it from global library setting or category
@ -397,7 +458,7 @@ class AnimelibPresenter(
*
* @return an observable of tracked anime.
*/
private fun getFilterObservable(): Observable<Map<Long, Map<Int, Boolean>>> {
private fun getFilterObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
return getTracksObservable().combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { tracks, _ -> tracks }
}
@ -406,16 +467,20 @@ class AnimelibPresenter(
*
* @return an observable of tracked anime.
*/
private fun getTracksObservable(): Observable<Map<Long, Map<Int, Boolean>>> {
return db.getTracks().asRxObservable().map { tracks ->
tracks.groupBy { it.anime_id }
.mapValues { tracksForAnimeId ->
// Check if any of the trackers is logged in for the current anime id
tracksForAnimeId.value.associate {
Pair(it.sync_id, trackManager.getService(it.sync_id.toLong())?.isLogged ?: false)
private fun getTracksObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
// TODO: Move this to domain/data layer
return getTracks.subscribe()
.asObservable().map { tracks ->
tracks
.groupBy { it.animeId }
.mapValues { tracksForAnimeId ->
// Check if any of the trackers is logged in for the current manga id
tracksForAnimeId.value.associate {
Pair(it.syncId, trackManager.getService(it.syncId)?.isLogged ?: false)
}
}
}
}.observeOn(Schedulers.io())
}
.observeOn(Schedulers.io())
}
/**
@ -452,11 +517,11 @@ class AnimelibPresenter(
*
* @param animes the list of anime.
*/
fun getCommonCategories(animes: List<Anime>): Collection<Category> {
suspend fun getCommonCategories(animes: List<Anime>): Collection<Category> {
if (animes.isEmpty()) return emptyList()
return animes.toSet()
.map { db.getCategoriesForAnime(it).executeAsBlocking() }
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2).toMutableList() }
.map { getCategories.await(it.id!!).map { it.toDbCategory() } }
.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
}
/**
@ -464,9 +529,9 @@ class AnimelibPresenter(
*
* @param animes the list of anime.
*/
fun getMixCategories(animes: List<Anime>): Collection<Category> {
suspend fun getMixCategories(animes: List<Anime>): Collection<Category> {
if (animes.isEmpty()) return emptyList()
val animeCategories = animes.toSet().map { db.getCategoriesForAnime(it).executeAsBlocking() }
val animeCategories = animes.toSet().map { getCategories.await(it.id!!).map { it.toDbCategory() } }
val common = animeCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
return animeCategories.flatten().distinct().subtract(common).toMutableList()
}
@ -479,8 +544,9 @@ class AnimelibPresenter(
fun downloadUnseenEpisodes(animes: List<Anime>) {
animes.forEach { anime ->
launchIO {
val episodes = db.getEpisodes(anime).executeAsBlocking()
val episodes = getEpisodeByAnimeId.await(anime.id!!)
.filter { !it.seen }
.map { it.toDbEpisode() }
downloadManager.downloadEpisodes(anime, episodes)
}
@ -495,17 +561,20 @@ class AnimelibPresenter(
fun markSeenStatus(animes: List<Anime>, seen: Boolean) {
animes.forEach { anime ->
launchIO {
val episodes = db.getEpisodes(anime).executeAsBlocking()
episodes.forEach {
it.seen = seen
if (!seen) {
it.last_second_seen = 0
val episodes = getEpisodeByAnimeId.await(anime.id!!)
val toUpdate = episodes
.map { chapter ->
EpisodeUpdate(
seen = seen,
lastSecondSeen = if (seen) 0 else null,
id = chapter.id,
)
}
}
db.updateEpisodesProgress(episodes).executeAsBlocking()
updateEpisode.awaitAll(toUpdate)
if (seen && preferences.removeAfterMarkedAsRead()) {
deleteEpisodes(anime, episodes)
deleteEpisodes(anime, episodes.map { it.toDbEpisode() })
}
}
}
@ -520,20 +589,23 @@ class AnimelibPresenter(
/**
* Remove the selected anime.
*
* @param animes the list of anime to delete.
* @param animeList the list of anime to delete.
* @param deleteFromAnimelib whether to delete anime from animelib.
* @param deleteEpisodes whether to delete downloaded episodes.
*/
fun removeAnimes(animes: List<Anime>, deleteFromAnimelib: Boolean, deleteEpisodes: Boolean) {
fun removeAnimes(animeList: List<Anime>, deleteFromAnimelib: Boolean, deleteEpisodes: Boolean) {
launchIO {
val animeToDelete = animes.distinctBy { it.id }
val animeToDelete = animeList.distinctBy { it.id }
if (deleteFromAnimelib) {
animeToDelete.forEach {
it.favorite = false
val toDelete = animeToDelete.map {
it.removeCovers(coverCache)
AnimeUpdate(
favorite = false,
id = it.id!!,
)
}
db.insertAnimes(animeToDelete).executeAsBlocking()
updateAnime.awaitAll(toDelete)
}
if (deleteEpisodes) {
@ -548,35 +620,22 @@ class AnimelibPresenter(
}
/**
* Move the given list of anime to categories.
* Bulk update categories of anime using old and new common categories.
*
* @param categories the selected categories.
* @param animes the list of anime to move.
*/
fun moveAnimesToCategories(categories: List<Category>, animes: List<Anime>) {
val mc = mutableListOf<AnimeCategory>()
for (anime in animes) {
categories.mapTo(mc) { AnimeCategory.create(anime, it) }
}
db.setAnimeCategories(mc, animes)
}
/**
* Bulk update categories of animes using old and new common categories.
*
* @param animes the list of anime to move.
* @param animeList the list of anime to move.
* @param addCategories the categories to add for all animes.
* @param removeCategories the categories to remove in all animes.
*/
fun updateAnimesToCategories(animes: List<Anime>, addCategories: List<Category>, removeCategories: List<Category>) {
val animeCategories = animes.map { anime ->
val categories = db.getCategoriesForAnime(anime).executeAsBlocking()
.subtract(removeCategories).plus(addCategories).distinct()
categories.map { AnimeCategory.create(anime, it) }
}.flatten()
db.setAnimeCategories(animeCategories, animes)
fun setAnimeCategories(animeList: List<Anime>, addCategories: List<Category>, removeCategories: List<Category>) {
presenterScope.launchIO {
animeList.map { anime ->
val categoryIds = getCategories.await(anime.id!!)
.map { it.toDbCategory() }
.subtract(removeCategories)
.plus(addCategories)
.mapNotNull { it.id?.toLong() }
setAnimeCategories.await(anime.id!!, categoryIds)
}
}
}
}

View file

@ -4,8 +4,9 @@ import android.content.Context
import android.util.AttributeSet
import android.view.View
import com.bluelinelabs.conductor.Router
import eu.kanade.domain.category.interactor.UpdateCategoryAnime
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.MangaTrackService
@ -14,9 +15,13 @@ import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -24,13 +29,15 @@ import uy.kohesive.injekt.injectLazy
class AnimelibSettingsSheet(
router: Router,
private val trackManager: TrackManager = Injekt.get(),
private val updateCategory: UpdateCategoryAnime = Injekt.get(),
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit,
) : TabbedBottomSheetDialog(router.activity!!) {
val filters: Filter
private val sort: Sort
private val display: Display
private val db: AnimeDatabaseHelper by injectLazy()
val sheetScope = CoroutineScope(Job() + Dispatchers.IO)
init {
filters = Filter(router.activity!!)
@ -253,7 +260,14 @@ class AnimelibSettingsSheet(
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
currentCategory?.sortDirection = flag.flag
db.insertCategory(currentCategory!!).executeAsBlocking()
sheetScope.launchIO {
updateCategory.await(
CategoryUpdate(
id = currentCategory!!.id?.toLong()!!,
flags = currentCategory!!.flags.toLong(),
),
)
}
} else {
preferences.librarySortingAscending().set(flag)
}
@ -275,7 +289,14 @@ class AnimelibSettingsSheet(
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
currentCategory?.sortMode = flag.flag
db.insertCategory(currentCategory!!).executeAsBlocking()
sheetScope.launchIO {
updateCategory.await(
CategoryUpdate(
id = currentCategory!!.id?.toLong()!!,
flags = currentCategory!!.flags.toLong(),
),
)
}
} else {
preferences.librarySortingMode().set(flag)
}
@ -364,7 +385,14 @@ class AnimelibSettingsSheet(
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
currentCategory?.displayMode = flag.flag
db.insertCategory(currentCategory!!).executeAsBlocking()
sheetScope.launchIO {
updateCategory.await(
CategoryUpdate(
id = currentCategory!!.id?.toLong()!!,
flags = currentCategory!!.flags.toLong(),
),
)
}
} else {
preferences.libraryDisplayMode().set(flag)
}

View file

@ -373,7 +373,7 @@ open class BrowseAnimeSourcePresenter(
* @return Array of category ids the anime is in, if none returns default id
*/
fun getAnimeCategoryIds(anime: Anime): Array<Long?> {
val categories = db.getCategoriesForAnime(anime).executeAsBlocking()
val categories = db.getCategoriesForAnime(anime.id!!).executeAsBlocking()
return categories.mapNotNull { it?.id?.toLong() }.toTypedArray()
}

View file

@ -9,7 +9,7 @@ import eu.kanade.domain.anime.model.toDbAnime
import eu.kanade.domain.animetrack.interactor.GetAnimeTracks
import eu.kanade.domain.animetrack.interactor.InsertAnimeTrack
import eu.kanade.domain.category.interactor.GetCategoriesAnime
import eu.kanade.domain.category.interactor.MoveAnimeToCategories
import eu.kanade.domain.category.interactor.SetAnimeCategories
import eu.kanade.domain.episode.interactor.GetEpisodeByAnimeId
import eu.kanade.domain.episode.interactor.SyncEpisodesWithSource
import eu.kanade.domain.episode.interactor.UpdateEpisode
@ -48,7 +48,7 @@ class AnimeSearchPresenter(
private val getCategories: GetCategoriesAnime = Injekt.get(),
private val getTracks: GetAnimeTracks = Injekt.get(),
private val insertTrack: InsertAnimeTrack = Injekt.get(),
private val moveAnimeToCategories: MoveAnimeToCategories = Injekt.get(),
private val setAnimeCategories: SetAnimeCategories = Injekt.get(),
) : GlobalAnimeSearchPresenter(initialQuery) {
private val replacingAnimeRelay = BehaviorRelay.create<Pair<Boolean, Anime?>>()
@ -164,7 +164,7 @@ class AnimeSearchPresenter(
// Update categories
if (migrateCategories) {
val categoryIds = getCategories.await(prevDomainAnime.id).map { it.id }
moveAnimeToCategories.await(domainAnime.id, categoryIds)
setAnimeCategories.await(domainAnime.id, categoryIds)
}
// Update track

View file

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.MoveMangaToCategories
import eu.kanade.domain.category.interactor.SetMangaCategories
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.UpdateChapter
@ -48,7 +48,7 @@ class SearchPresenter(
private val getCategories: GetCategories = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
private val moveMangaToCategories: MoveMangaToCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
) : GlobalSearchPresenter(initialQuery) {
private val replacingMangaRelay = BehaviorRelay.create<Pair<Boolean, Manga?>>()
@ -164,7 +164,7 @@ class SearchPresenter(
// Update categories
if (migrateCategories) {
val categoryIds = getCategories.await(prevDomainManga.id).map { it.id }
moveMangaToCategories.await(domainManga.id, categoryIds)
setMangaCategories.await(domainManga.id, categoryIds)
}
// Update track

View file

@ -373,7 +373,7 @@ open class BrowseSourcePresenter(
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): Array<Long?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
val categories = db.getCategoriesForManga(manga.id!!).executeAsBlocking()
return categories.mapNotNull { it?.id?.toLong() }.toTypedArray()
}

View file

@ -29,6 +29,8 @@ import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser
@ -36,6 +38,7 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
import eu.kanade.tachiyomi.widget.EmptyView
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -226,6 +229,7 @@ class LibraryController(
destroyActionModeIfNeeded()
adapter?.onDestroy()
adapter = null
settingsSheet?.sheetScope?.cancel()
settingsSheet = null
tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null
@ -541,25 +545,29 @@ class LibraryController(
* Move the selected manga to a list of categories.
*/
private fun showMangaCategoriesDialog() {
// Create a copy of selected manga
val mangas = selectedMangas.toList()
viewScope.launchIO {
// Create a copy of selected manga
val mangas = selectedMangas.toList()
// Hide the default category because it has a different behavior than the ones from db.
val categories = presenter.categories.filter { it.id != 0 }
// Hide the default category because it has a different behavior than the ones from db.
val categories = presenter.categories.filter { it.id != 0 }
// Get indexes of the common categories to preselect.
val common = presenter.getCommonCategories(mangas)
// Get indexes of the mix categories to preselect.
val mix = presenter.getMixCategories(mangas)
val preselected = categories.map {
when (it) {
in common -> QuadStateTextView.State.CHECKED.ordinal
in mix -> QuadStateTextView.State.INDETERMINATE.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
// Get indexes of the common categories to preselect.
val common = presenter.getCommonCategories(mangas)
// Get indexes of the mix categories to preselect.
val mix = presenter.getMixCategories(mangas)
val preselected = categories.map {
when (it) {
in common -> QuadStateTextView.State.CHECKED.ordinal
in mix -> QuadStateTextView.State.INDETERMINATE.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
}
}.toIntArray()
launchUI {
ChangeMangaCategoriesDialog(this@LibraryController, mangas, categories, preselected)
.showDialog(router)
}
}.toIntArray()
ChangeMangaCategoriesDialog(this, mangas, categories, preselected)
.showDialog(router)
}
}
private fun downloadUnreadChapters() {
@ -579,7 +587,7 @@ class LibraryController(
}
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
presenter.updateMangasToCategories(mangas, addCategories, removeCategories)
presenter.setMangaCategories(mangas, addCategories, removeCategories)
destroyActionModeIfNeeded()
}

View file

@ -2,12 +2,23 @@ package eu.kanade.tachiyomi.ui.library
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.core.util.asObservable
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.SetMangaCategories
import eu.kanade.domain.category.model.toDbCategory
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.MangaUpdate
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
@ -23,6 +34,8 @@ import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -47,7 +60,13 @@ private typealias LibraryMap = Map<Int, List<LibraryItem>>
* Presenter of [LibraryController].
*/
class LibraryPresenter(
private val db: DatabaseHelper = Injekt.get(),
private val handler: DatabaseHandler = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
@ -92,6 +111,7 @@ class LibraryPresenter(
* Subscribes to library if needed.
*/
fun subscribeLibrary() {
// TODO: Move this to a coroutine world
if (librarySubscription.isNullOrUnsubscribed()) {
librarySubscription = getLibraryObservable()
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
@ -115,7 +135,7 @@ class LibraryPresenter(
*
* @param map the map to filter.
*/
private fun applyFilters(map: LibraryMap, trackMap: Map<Long, Map<Int, Boolean>>): LibraryMap {
private fun applyFilters(map: LibraryMap, trackMap: Map<Long, Map<Long, Boolean>>): LibraryMap {
val downloadedOnly = preferences.downloadedOnly().get()
val filterDownloaded = preferences.filterDownloaded().get()
val filterUnread = preferences.filterUnread().get()
@ -252,18 +272,30 @@ class LibraryPresenter(
private fun applySort(categories: List<Category>, map: LibraryMap): LibraryMap {
val lastReadManga by lazy {
var counter = 0
// Result comes as newest to oldest so it's reversed
db.getLastReadManga().executeAsBlocking().reversed().associate { it.id!! to counter++ }
// TODO: Make [applySort] a suspended function
runBlocking {
handler.awaitList {
mangasQueries.getLastRead()
}.associate { it._id to counter++ }
}
}
val latestChapterManga by lazy {
var counter = 0
// Result comes as newest to oldest so it's reversed
db.getLatestChapterManga().executeAsBlocking().reversed().associate { it.id!! to counter++ }
// TODO: Make [applySort] a suspended function
runBlocking {
handler.awaitList {
mangasQueries.getLatestByChapterUploadDate()
}.associate { it._id to counter++ }
}
}
val chapterFetchDateManga by lazy {
var counter = 0
// Result comes as newest to oldest so it's reversed
db.getChapterFetchDateManga().executeAsBlocking().reversed().associate { it.id!! to counter++ }
// TODO: Make [applySort] a suspended function
runBlocking {
handler.awaitList {
mangasQueries.getLatestByChapterFetchDate()
}.associate { it._id to counter++ }
}
}
val sortingModes = categories.associate { category ->
@ -366,7 +398,7 @@ class LibraryPresenter(
* @return an observable of the categories.
*/
private fun getCategoriesObservable(): Observable<List<Category>> {
return db.getCategories().asRxObservable()
return getCategories.subscribe().map { it.map { it.toDbCategory() } }.asObservable()
}
/**
@ -378,7 +410,36 @@ class LibraryPresenter(
private fun getLibraryMangasObservable(): Observable<LibraryMap> {
val defaultLibraryDisplayMode = preferences.libraryDisplayMode()
val shouldSetFromCategory = preferences.categorizedDisplaySettings()
return db.getLibraryMangas().asRxObservable()
// TODO: Move this to domain/data layer
return handler
.subscribeToList {
mangasQueries.getLibrary { _id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List<String>?, title: String, status: Long, thumbnail_url: String?, favorite: Boolean, last_update: Long?, next_update: Long?, initialized: Boolean, viewer: Long, chapter_flags: Long, cover_last_modified: Long, date_added: Long, unread_count: Long, read_count: Long, category: Long ->
LibraryManga().apply {
this.id = _id
this.source = source
this.url = url
this.artist = artist
this.author = author
this.description = description
this.genre = genre?.joinToString()
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnail_url
this.favorite = favorite
this.last_update = last_update ?: 0
this.initialized = initialized
this.viewer_flags = viewer.toInt()
this.chapter_flags = chapter_flags.toInt()
this.cover_last_modified = cover_last_modified
this.date_added = date_added
this.unreadCount = unread_count.toInt()
this.readCount = read_count.toInt()
this.category = category.toInt()
}
}
}
.asObservable()
.map { list ->
list.map { libraryManga ->
// Display mode based on user preference: take it from global library setting or category
@ -396,7 +457,7 @@ class LibraryPresenter(
*
* @return an observable of tracked manga.
*/
private fun getFilterObservable(): Observable<Map<Long, Map<Int, Boolean>>> {
private fun getFilterObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
return getTracksObservable().combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { tracks, _ -> tracks }
}
@ -405,16 +466,20 @@ class LibraryPresenter(
*
* @return an observable of tracked manga.
*/
private fun getTracksObservable(): Observable<Map<Long, Map<Int, Boolean>>> {
return db.getTracks().asRxObservable().map { tracks ->
tracks.groupBy { it.manga_id }
.mapValues { tracksForMangaId ->
// Check if any of the trackers is logged in for the current manga id
tracksForMangaId.value.associate {
Pair(it.sync_id, trackManager.getService(it.sync_id.toLong())?.isLogged ?: false)
private fun getTracksObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
// TODO: Move this to domain/data layer
return getTracks.subscribe()
.asObservable().map { tracks ->
tracks
.groupBy { it.mangaId }
.mapValues { tracksForMangaId ->
// Check if any of the trackers is logged in for the current manga id
tracksForMangaId.value.associate {
Pair(it.syncId, trackManager.getService(it.syncId)?.isLogged ?: false)
}
}
}
}.observeOn(Schedulers.io())
}
.observeOn(Schedulers.io())
}
/**
@ -451,11 +516,11 @@ class LibraryPresenter(
*
* @param mangas the list of manga.
*/
fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
return mangas.toSet()
.map { db.getCategoriesForManga(it).executeAsBlocking() }
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2).toMutableList() }
.map { getCategories.await(it.id!!).map { it.toDbCategory() } }
.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
}
/**
@ -463,9 +528,9 @@ class LibraryPresenter(
*
* @param mangas the list of manga.
*/
fun getMixCategories(mangas: List<Manga>): Collection<Category> {
suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
val mangaCategories = mangas.toSet().map { db.getCategoriesForManga(it).executeAsBlocking() }
val mangaCategories = mangas.toSet().map { getCategories.await(it.id!!).map { it.toDbCategory() } }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
return mangaCategories.flatten().distinct().subtract(common).toMutableList()
}
@ -478,8 +543,9 @@ class LibraryPresenter(
fun downloadUnreadChapters(mangas: List<Manga>) {
mangas.forEach { manga ->
launchIO {
val chapters = db.getChapters(manga).executeAsBlocking()
val chapters = getChapterByMangaId.await(manga.id!!)
.filter { !it.read }
.map { it.toDbChapter() }
downloadManager.downloadChapters(manga, chapters)
}
@ -494,17 +560,20 @@ class LibraryPresenter(
fun markReadStatus(mangas: List<Manga>, read: Boolean) {
mangas.forEach { manga ->
launchIO {
val chapters = db.getChapters(manga).executeAsBlocking()
chapters.forEach {
it.read = read
if (!read) {
it.last_page_read = 0
val chapters = getChapterByMangaId.await(manga.id!!)
val toUpdate = chapters
.map { chapter ->
ChapterUpdate(
read = read,
lastPageRead = if (read) 0 else null,
id = chapter.id,
)
}
}
db.updateChaptersProgress(chapters).executeAsBlocking()
updateChapter.awaitAll(toUpdate)
if (read && preferences.removeAfterMarkedAsRead()) {
deleteChapters(manga, chapters)
deleteChapters(manga, chapters.map { it.toDbChapter() })
}
}
}
@ -519,20 +588,23 @@ class LibraryPresenter(
/**
* Remove the selected manga.
*
* @param mangas the list of manga to delete.
* @param mangaList the list of manga to delete.
* @param deleteFromLibrary whether to delete manga from library.
* @param deleteChapters whether to delete downloaded chapters.
*/
fun removeMangas(mangas: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
fun removeMangas(mangaList: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
launchIO {
val mangaToDelete = mangas.distinctBy { it.id }
val mangaToDelete = mangaList.distinctBy { it.id }
if (deleteFromLibrary) {
mangaToDelete.forEach {
it.favorite = false
val toDelete = mangaToDelete.map {
it.removeCovers(coverCache)
MangaUpdate(
favorite = false,
id = it.id!!,
)
}
db.insertMangas(mangaToDelete).executeAsBlocking()
updateManga.awaitAll(toDelete)
}
if (deleteChapters) {
@ -547,35 +619,22 @@ class LibraryPresenter(
}
/**
* Move the given list of manga to categories.
* Bulk update categories of manga using old and new common categories.
*
* @param categories the selected categories.
* @param mangas the list of manga to move.
*/
fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
val mc = mutableListOf<MangaCategory>()
for (manga in mangas) {
categories.mapTo(mc) { MangaCategory.create(manga, it) }
}
db.setMangaCategories(mc, mangas)
}
/**
* Bulk update categories of mangas using old and new common categories.
*
* @param mangas the list of manga to move.
* @param mangaList the list of manga to move.
* @param addCategories the categories to add for all mangas.
* @param removeCategories the categories to remove in all mangas.
*/
fun updateMangasToCategories(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
val mangaCategories = mangas.map { manga ->
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
.subtract(removeCategories).plus(addCategories).distinct()
categories.map { MangaCategory.create(manga, it) }
}.flatten()
db.setMangaCategories(mangaCategories, mangas)
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
presenterScope.launchIO {
mangaList.map { manga ->
val categoryIds = getCategories.await(manga.id!!)
.map { it.toDbCategory() }
.subtract(removeCategories)
.plus(addCategories)
.mapNotNull { it.id?.toLong() }
setMangaCategories.await(manga.id!!, categoryIds)
}
}
}
}

View file

@ -4,8 +4,9 @@ import android.content.Context
import android.util.AttributeSet
import android.view.View
import com.bluelinelabs.conductor.Router
import eu.kanade.domain.category.interactor.UpdateCategory
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.AnimeTrackService
@ -14,9 +15,13 @@ import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -24,13 +29,15 @@ import uy.kohesive.injekt.injectLazy
class LibrarySettingsSheet(
router: Router,
private val trackManager: TrackManager = Injekt.get(),
private val updateCategory: UpdateCategory = Injekt.get(),
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit,
) : TabbedBottomSheetDialog(router.activity!!) {
val filters: Filter
private val sort: Sort
private val display: Display
private val db: DatabaseHelper by injectLazy()
val sheetScope = CoroutineScope(Job() + Dispatchers.IO)
init {
filters = Filter(router.activity!!)
@ -252,8 +259,14 @@ class LibrarySettingsSheet(
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
currentCategory?.sortDirection = flag.flag
db.insertCategory(currentCategory!!).executeAsBlocking()
sheetScope.launchIO {
updateCategory.await(
CategoryUpdate(
id = currentCategory!!.id?.toLong()!!,
flags = currentCategory!!.flags.toLong(),
),
)
}
} else {
preferences.librarySortingAscending().set(flag)
}
@ -274,8 +287,14 @@ class LibrarySettingsSheet(
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
currentCategory?.sortMode = flag.flag
db.insertCategory(currentCategory!!).executeAsBlocking()
sheetScope.launchIO {
updateCategory.await(
CategoryUpdate(
id = currentCategory!!.id?.toLong()!!,
flags = currentCategory!!.flags.toLong(),
),
)
}
} else {
preferences.librarySortingMode().set(flag)
}
@ -363,8 +382,14 @@ class LibrarySettingsSheet(
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
currentCategory?.displayMode = flag.flag
db.insertCategory(currentCategory!!).executeAsBlocking()
sheetScope.launchIO {
updateCategory.await(
CategoryUpdate(
id = currentCategory!!.id?.toLong()!!,
flags = currentCategory!!.flags.toLong(),
),
)
}
} else {
preferences.libraryDisplayMode().set(flag)
}

View file

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.manga
import android.os.Bundle
import androidx.compose.runtime.Immutable
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.MoveMangaToCategories
import eu.kanade.domain.category.interactor.SetMangaCategories
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.chapter.interactor.UpdateChapter
@ -91,7 +91,7 @@ class MangaPresenter(
private val getCategories: GetCategories = Injekt.get(),
private val deleteTrack: DeleteTrack = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val moveMangaToCategories: MoveMangaToCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
) : BasePresenter<MangaController>() {
@ -359,7 +359,7 @@ class MangaPresenter(
val mangaId = manga.id ?: return
val categoryIds = categories.mapNotNull { it.id?.toLong() }
presenterScope.launchIO {
moveMangaToCategories.await(mangaId, categoryIds)
setMangaCategories.await(mangaId, categoryIds)
}
}

View file

@ -93,7 +93,7 @@ class PlayerPresenter(
private fun initEpisodeList(): List<Episode> {
val anime = anime!!
val dbEpisodes = db.getEpisodes(anime).executeAsBlocking()
val dbEpisodes = db.getEpisodes(anime.id!!).executeAsBlocking()
val selectedEpisode = dbEpisodes.find { it.id == episodeId }
?: error("Requested episode of id $episodeId not found in episode list")
@ -338,7 +338,7 @@ class PlayerPresenter(
else -> throw NotImplementedError("Unknown sorting method")
}
val episodes = db.getEpisodes(anime).executeAsBlocking()
val episodes = db.getEpisodes(anime.id!!).executeAsBlocking()
.sortedWith { e1, e2 -> sortFunction(e1, e2) }
val currentEpisodePosition = episodes.indexOf(episode)

View file

@ -118,7 +118,7 @@ class ReaderPresenter(
*/
private val chapterList by lazy {
val manga = manga!!
val dbChapters = db.getChapters(manga).executeAsBlocking()
val dbChapters = db.getChapters(manga.id!!).executeAsBlocking()
val selectedChapter = dbChapters.find { it.id == chapterId }
?: error("Requested chapter of id $chapterId not found in chapter list")

View file

@ -74,7 +74,7 @@ fun DomainAnime.shouldDownloadNewEpisodes(db: AnimeDatabaseHelper, prefs: Prefer
// Get all categories, else default category (0)
val categoriesForAnime =
db.getCategoriesForAnime(toDbAnime()).executeAsBlocking()
db.getCategoriesForAnime(id).executeAsBlocking()
.mapNotNull { it.id }
.takeUnless { it.isEmpty() } ?: listOf(0)

View file

@ -75,7 +75,7 @@ fun DomainManga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: Preferences
// Get all categories, else default category (0)
val categoriesForManga =
db.getCategoriesForManga(toDbManga()).executeAsBlocking()
db.getCategoriesForManga(id).executeAsBlocking()
.mapNotNull { it.id }
.takeUnless { it.isEmpty() } ?: listOf(0)

View file

@ -185,7 +185,7 @@ object ImageUtil {
* @return true if the height:width ratio is greater than 3.
*/
private fun isTallImage(imageStream: InputStream): Boolean {
val options = extractImageOptions(imageStream, false)
val options = extractImageOptions(imageStream, resetAfterExtraction = false)
return (options.outHeight / options.outWidth) > 3
}
@ -197,16 +197,34 @@ object ImageUtil {
return true
}
val options = extractImageOptions(imageFile.openInputStream(), false).apply { inJustDecodeBounds = false }
val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { inJustDecodeBounds = false }
// Values are stored as they get modified during split loop
val imageHeight = options.outHeight
val imageWidth = options.outWidth
val splitHeight = getDisplayMaxHeightInPx
val splitHeight = (getDisplayMaxHeightInPx * 1.5).toInt()
// -1 so it doesn't try to split when imageHeight = getDisplayHeightInPx
val partCount = (imageHeight - 1) / getDisplayMaxHeightInPx + 1
val partCount = (imageHeight - 1) / splitHeight + 1
logcat { "Splitting ${imageHeight}px height image into $partCount part with estimated ${splitHeight}px per height" }
val optimalSplitHeight = imageHeight / partCount
val splitDataList = (0 until partCount).fold(mutableListOf<SplitData>()) { list, index ->
list.apply {
// Only continue if the list is empty or there is image remaining
if (isEmpty() || imageHeight > last().bottomOffset) {
val topOffset = index * optimalSplitHeight
var outputImageHeight = min(optimalSplitHeight, imageHeight - topOffset)
val remainingHeight = imageHeight - (topOffset + outputImageHeight)
// If remaining height is smaller or equal to 1/3th of
// optimal split height then include it in current page
if (remainingHeight <= (optimalSplitHeight / 3)) {
outputImageHeight += remainingHeight
}
add(SplitData(index, topOffset, outputImageHeight))
}
}
}
val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(imageFile.openInputStream())
@ -220,36 +238,52 @@ object ImageUtil {
return false
}
try {
(0 until partCount).forEach { splitIndex ->
val splitPath = imageFilePath.substringBeforeLast(".") + "__${"%03d".format(splitIndex + 1)}.jpg"
logcat {
"Splitting image with height of $imageHeight into $partCount part " +
"with estimated ${optimalSplitHeight}px height per split"
}
val topOffset = splitIndex * splitHeight
val outputImageHeight = min(splitHeight, imageHeight - topOffset)
val bottomOffset = topOffset + outputImageHeight
logcat { "Split #$splitIndex with topOffset=$topOffset height=$outputImageHeight bottomOffset=$bottomOffset" }
return try {
splitDataList.forEach { splitData ->
val splitPath = splitImagePath(imageFilePath, splitData.index)
val region = Rect(0, topOffset, imageWidth, bottomOffset)
val region = Rect(0, splitData.topOffset, imageWidth, splitData.bottomOffset)
FileOutputStream(splitPath).use { outputStream ->
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, options)
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
splitBitmap.recycle()
}
logcat {
"Success: Split #${splitData.index + 1} with topOffset=${splitData.topOffset} " +
"height=${splitData.outputImageHeight} bottomOffset=${splitData.bottomOffset}"
}
}
imageFile.delete()
return true
true
} catch (e: Exception) {
// Image splits were not successfully saved so delete them and keep the original image
(0 until partCount)
.map { imageFilePath.substringBeforeLast(".") + "__${"%03d".format(it + 1)}.jpg" }
splitDataList
.map { splitImagePath(imageFilePath, it.index) }
.forEach { File(it).delete() }
logcat(LogPriority.ERROR, e)
return false
false
} finally {
bitmapRegionDecoder.recycle()
}
}
private fun splitImagePath(imageFilePath: String, index: Int) =
imageFilePath.substringBeforeLast(".") + "__${"%03d".format(index + 1)}.jpg"
data class SplitData(
val index: Int,
val topOffset: Int,
val outputImageHeight: Int,
) {
val bottomOffset = topOffset + outputImageHeight
}
/**
* Algorithm for determining what background to accompany a comic/manga page
*/

View file

@ -10,7 +10,7 @@
<string name="label_library">Bibliotēka</string>
<string name="label_recent_manga">Vēsture</string>
<string name="label_recent_updates">Atjauninājumi</string>
<string name="label_backup">Rezerves kopija</string>
<string name="label_backup">Dublēšana un atjaunošana</string>
<string name="action_settings">Iestatījumi</string>
<string name="action_filter">Filtrs</string>
<string name="action_filter_downloaded">Lejupieladēts</string>
@ -76,12 +76,12 @@
<string name="download_notifier_downloader_title">Lejupielādētājs</string>
<string name="information_webview_outdated">Lai uzlabotu saderību, lūdzu, atjauniniet WebView lietotni</string>
<string name="information_webview_required">Tachiyomi ir nepieciešams WebView</string>
<string name="restoring_backup_error">Dublējuma atjaunināšana neizdevās</string>
<string name="restoring_backup">Atjaunina dublējumu</string>
<string name="restore_in_progress">Atjaunināšana jau notiek</string>
<string name="restoring_backup_error">Dublējuma atjaunošana neizdevās</string>
<string name="restoring_backup">Dublējuma atjaunošana</string>
<string name="restore_in_progress">Atjaunošana jau notiek</string>
<string name="creating_backup_error">Dublēšana neizdevās</string>
<string name="creating_backup">Taisa dublējumu</string>
<string name="backup_choice">Ko tu gribi dublēt\?</string>
<string name="creating_backup">Izveido dublējumu</string>
<string name="backup_choice">Ko vēlaties dublēt\?</string>
<string name="backup_in_progress">Dublēšana jau notiek</string>
<plurals name="restore_completed_message">
<item quantity="zero">Pabeigts %1$s ar %2$s kļūdām</item>
@ -89,9 +89,9 @@
<item quantity="other">Pabeigts %1$s ar %2$s kļūdām</item>
</plurals>
<string name="restore_duration">%02d min, %02d sec</string>
<string name="restore_completed">Atjaunināšana pabeigta</string>
<string name="backup_restore_missing_sources">Trūkst avotu:</string>
<string name="invalid_backup_file_missing_manga">Dublējums nesatur nekādu mangu.</string>
<string name="restore_completed">Atjaunošana pabeigta</string>
<string name="backup_restore_missing_sources">Trūkstošie avoti:</string>
<string name="invalid_backup_file_missing_manga">Dublējumā nav nevienas manga.</string>
<string name="label_extension_info">Paplašinājumu informācija</string>
<string name="label_extensions">Paplašinājumi</string>
<string name="unlock_app">Atslēgt Anijomi</string>
@ -99,7 +99,7 @@
<string name="tapping_inverted_vertical">Vertikāls</string>
<string name="tapping_inverted_horizontal">Horizontāls</string>
<string name="tapping_inverted_none">Nav</string>
<string name="pref_read_with_tapping_inverted">Invertēt spiešanu</string>
<string name="pref_read_with_tapping_inverted">Invertētas skāriena zonas</string>
<string name="channel_ext_updates">Paplašinājumu atjauninājumi</string>
<string name="channel_new_chapters_episodes">Nodaļu atjauninājumi</string>
<string name="download_notifier_download_paused_chapters">Lejuplāde pauzēta</string>
@ -123,7 +123,7 @@
<string name="action_menu">Izvēlne</string>
<string name="confirm_exit">Vēlreiz nospiediet atpakaļ, lai izietu</string>
<string name="information_empty_category">Jums nav nevienas kategorijas. Pieskarieties plus pogu, lai izveidotu vienu savas bibliotēkas organizēšanai.</string>
<string name="information_empty_library">Jūsu bibliotēka ir tukša. Pievienojiet sērijas savai bibliotēkai no Pārlūka.</string>
<string name="information_empty_library">Jūsu bibliotēka ir tukša</string>
<string name="information_no_recent_manga">Nekas nesen lasīts</string>
<string name="information_no_recent">Nekas nesen lasīts</string>
<string name="action_newest">Jaunākais</string>
@ -168,8 +168,8 @@
<string name="action_move_to_bottom">Pārvietot uz leju</string>
<string name="action_move_to_top">Pārvietot uz augšu</string>
<string name="action_oldest">Vecākais</string>
<string name="action_display_download_badge">Lejuplādes žetoni</string>
<string name="action_display_unread_badge">Nelasītas nozīmes</string>
<string name="action_display_download_badge">Lejuplādēt nodaļas</string>
<string name="action_display_unread_badge">Nelasītās nodaļas</string>
<string name="action_desc">Dilstoši</string>
<string name="action_asc">Augoši</string>
<string name="action_order_by_chapter_number">Pēc nodaļas numura</string>
@ -186,12 +186,12 @@
<string name="title">Nosaukums</string>
<string name="pref_category_delete_chapters">Izdzēst nodaļas</string>
<string name="pref_download_directory">Lejuplādes vieta</string>
<string name="pref_category_auto_download">Automātiska lejuplāde</string>
<string name="pref_category_auto_download">Automātiska lejupielāde</string>
<string name="pref_restore_backup">Atjaunot dublējumu</string>
<string name="backup_restore_missing_trackers">Nav pieslēgts pie izsekotājiems:</string>
<string name="backup_restore_content_full">Dati no dublējuma faila būs atjaunoti.
<string name="backup_restore_missing_trackers">Sekotāji, kas nav pieteikušies:</string>
<string name="backup_restore_content_full">Dublējuma faila dati tiks atjaunoti.
\n
\nJums būs nepieciešams instalēt jebkādus trūkstošos paplašinājumus un pēc tam pieslēgties izsekošanas pakalpojumiem.</string>
\nJums būs jāinstalē visi trūkstošie paplašinājumi un jāpiesakās izsekošanas pakalpojumos, lai tos izmantotu.</string>
<string name="unread">Neizlasītie</string>
<string name="all">Visi</string>
<string name="ext_update">Atjauninājums</string>
@ -330,4 +330,170 @@
<string name="update_48hour">Ik pēc 2 dienam</string>
<string name="update_72hour">Ik pēc 3 dienām</string>
<string name="connected_to_wifi">Tikai uz Wi-Fi</string>
<string name="automatic_background">Automātisks</string>
<string name="pref_keep_screen_on">Turēt ekrānu ieslēgtu</string>
<string name="zoom_start_center">Centrs</string>
<string name="rotation_type">Rotācijas tips</string>
<string name="rotation_free">Brīvs</string>
<string name="scale_type_original_size">Oriģinālais lielums</string>
<string name="zoom_start_automatic">Automātisks</string>
<string name="zoom_start_left">Pa kreisi</string>
<string name="zoom_start_right">Pa labi</string>
<string name="double_tap_anim_speed_normal">Normāls</string>
<string name="webtoon_side_padding_5">5%</string>
<string name="webtoon_side_padding_10">10%</string>
<string name="pref_remove_bookmarked_chapters">Atļaut dzēst ar grāmatzīmēm atzīmētās nodaļas</string>
<string name="last_read_chapter">Pēdējā lasītā nodaļa</string>
<string name="second_to_last">Pirmspēdējā nodaļa</string>
<string name="pref_download_new">Lejupielādē jaunās nodaļas</string>
<string name="rotation_reverse_portrait">Pretējs portrets</string>
<string name="black_background">Melns</string>
<string name="label_default">Noklusējums</string>
<string name="pref_reader_actions">Darbība</string>
<string name="color_filter_r_value">R</string>
<string name="color_filter_g_value">G</string>
<string name="color_filter_b_value">B</string>
<string name="color_filter_a_value">A</string>
<string name="pref_always_show_chapter_transition">Vienmēr rādīt nodaļu pāreju</string>
<string name="webtoon_side_padding_20">20%</string>
<string name="webtoon_side_padding_25">25%</string>
<string name="pref_hide_threshold">Jutīgums, lai ritinājumā paslēptu izvēlni</string>
<string name="rotation_force_landscape">Aizslēgts ainavas režīmā</string>
<string name="pref_lowest">Zemākais</string>
<string name="pref_remove_after_marked_as_read">Pēc manuālas atzīmēšanas kā lasītu</string>
<string name="save_chapter_as_cbz">Saglabāt kā CBZ arhīvu</string>
<string name="nav_zone_next">Nākamais</string>
<string name="l_nav">L formas</string>
<string name="webtoon_side_padding_15">15%</string>
<string name="gray_background">Pelēks</string>
<string name="disabled_nav">Atspējots</string>
<string name="white_background">Balts</string>
<string name="vertical_plus_viewer">Nepārtraukta vertikāle</string>
<string name="webtoon_side_padding_0">Nekāds</string>
<string name="pref_webtoon_side_padding">Sānu platums</string>
<string name="pref_remove_exclude_categories">Izslēgtās kategorijas</string>
<string name="action_display_local_badge">Lokālā manga</string>
<string name="pref_create_folder_per_manga">Saglabāt lappuses atsevišķās mapēs</string>
<string name="nav_zone_prev">Iepriekšējais</string>
<string name="pref_viewer_type">Noklusējuma lasīšanas režīms</string>
<string name="kindlish_nav">Kindle-ish</string>
<string name="nav_zone_left">Pa kreisi</string>
<string name="edge_nav">Mala</string>
<string name="right_and_left_nav">Pa labi un pa kreisi</string>
<string name="nav_zone_right">Pa labi</string>
<string name="left_to_right_viewer">No kreisās puses uz labo</string>
<string name="webtoon_viewer">Webtoon</string>
<string name="pref_viewer_nav">Skāriena zonas</string>
<string name="scale_type_stretch">Izstiept</string>
<string name="scale_type_fit_width">Ietilpt platumā</string>
<string name="scale_type_fit_height">Ietilpt augstumā</string>
<string name="double_tap_anim_speed_0">Nav animācijas</string>
<string name="rotation_landscape">Ainava</string>
<string name="rotation_portrait">Portrets</string>
<string name="rotation_force_portrait">Aizslēgts portreta režīmā</string>
<string name="pref_category_reading_mode">Lasīšanas režīms</string>
<string name="action_filter_started">Sākts</string>
<string name="right_to_left_viewer">No labās puses uz kreiso</string>
<string name="vertical_viewer">Vertikāls</string>
<string name="manga">Manga</string>
<string name="double_tap_anim_speed_fast">Ātrs</string>
<string name="pref_rotation_type">Noklusējuma rotācijas tips</string>
<string name="pref_high">Augsts</string>
<string name="pref_highest">Augstākais</string>
<string name="label_warning">Brīdinājums</string>
<string name="pref_skip_read_chapters">Izlaist izlasītās nodaļas</string>
<string name="custom_dir">Pielāgota lokācija</string>
<string name="pref_inverted_colors">Invertēts</string>
<string name="filter_mode_multiply">Reizināt</string>
<string name="filter_mode_screen">Ekrāns</string>
<string name="pref_skip_filtered_chapters">Izlaist filtrētās nodaļas</string>
<string name="pref_reader_navigation">Navigācija</string>
<string name="pref_read_with_volume_keys_inverted">Invertēt skaļuma regulēšanas taustiņus</string>
<string name="pref_read_with_long_tap">Rādīt ar ilgu pieskārienu</string>
<string name="pref_create_folder_per_manga_summary">Izveido mapes atbilstoši manga nosaukumam</string>
<string name="pref_reader_theme">Fona krāsa</string>
<string name="pref_image_scale_type">Mēroga tips</string>
<string name="scale_type_fit_screen">Ietilpt ekrānā</string>
<string name="scale_type_smart_fit">Viedā ietilpšana</string>
<string name="pref_zoom_start">Tālummaiņas sākuma pozīcija</string>
<string name="pref_low">Zems</string>
<string name="pref_remove_after_read">Pēc lasīšanas automātiski izdzēst nodaļas</string>
<string name="disabled">Atspējots</string>
<string name="pref_download_new_categories_details">Izslēgto kategoriju manga netiks lejupielādēta pat tad, ja tās ir arī iekļautajās kategorijās.</string>
<string name="split_tall_images">Automātiski sadalīt garus attēlus</string>
<string name="pref_read_with_volume_keys">Skaļuma regulēšanas taustiņi</string>
<string name="privacy_policy">Konfidencialitātes politika</string>
<string name="pref_create_backup_summ">Var izmantot, lai atjaunotu pašreizējo bibliotēku</string>
<string name="split_tall_images_summary">Uzlabo lasītāja veiktspēju, sadalot garus lejupielādētus attēlus.</string>
<string name="enhanced_tracking_info">Pakalpojumi, kas nodrošina uzlabotus līdzekļus konkrētiem avotiem. Pievienojot bibliotēkai, manga tiks automātiski izsekota.</string>
<string name="pref_duplicate_pinned_sources">Rādīt dublētus piespraustos avotus</string>
<string name="pref_duplicate_pinned_sources_summary">Vēlreiz rādīt piespraustos avotus atbilstošajās valodu grupās</string>
<string name="pref_create_backup">Izveidot dublējumu</string>
<string name="pref_backup_directory">Dublējuma atrašanās vieta</string>
<string name="invalid_backup_file">Nederīgs dublējuma fails</string>
<string name="pref_backup_service_category">Automātiskā dublēšana</string>
<string name="pref_backup_interval">Dublējumu biežums</string>
<string name="backup_info">Automātiskā dublēšana ir ļoti ieteicama. Kopijas vajadzētu glabāt arī citās vietās.</string>
<string name="about_dont_kill_my_app">Dažiem ražotājiem ir papildu lietojumprogrammu ierobežojumi, kas iznīcina fona pakalpojumus. Šajā vietnē ir vairāk informācijas par to, kā to izlabot.</string>
<string name="tracking_info">Vienvirziena sinhronizācija, lai atjauninātu sekošanas pakalpojumu nodaļas progresu. Iestatiet izsekošanu atsevišķiem manga ierakstiem, izmantojot sekošanas pogu.</string>
<string name="pref_disable_battery_optimization_summary">Palīdz ar fona bibliotēku atjauninājumiem un dublējumiem</string>
<string name="battery_optimization_disabled">Akumulatora optimizācija jau ir atspējota</string>
<string name="pref_verbose_logging_summary">Drukāt verbose žurnālus sistēmas žurnālā (samazina programmas veiktspēju)</string>
<string name="pref_auto_update_manga_sync">Atjaunot progresu pēc lasīšanas</string>
<string name="tracking_guide">Sekošanas rokasgrāmata</string>
<string name="pref_dump_crash_logs">Saglabā avārijas žurnālu</string>
<string name="pref_dump_crash_logs_summary">Saglabā kļūdu žurnālus failā priekš koplietošanas ar izstrādātājiem</string>
<string name="pref_search_pinned_sources_only">Iekļaut tikai piespraustos avotus</string>
<string name="pref_clear_chapter_cache">Notīrīt nodaļas kešatmiņu</string>
<string name="pref_refresh_library_covers">Atsvaidzināt bibliotēkas vākus</string>
<string name="clear_database_completed">Ieraksti izdzēsti</string>
<string name="empty_backup_error">Nav bibliotēkas ierakstu, ko dublēt</string>
<string name="restoring_backup_canceled">Atjaunošana atcelta</string>
<string name="restore_miui_warning">Dublēšana/atjaunošana var nedarboties pareizi, ja ir atspējota MIUI Optimization.</string>
<string name="pref_auto_clear_chapter_cache">Notīriet nodaļu kešatmiņu, aizverot lietotni</string>
<string name="clear_database_source_item_count">%1$d grāmatas, kas nav bibliotēkas, ir datu bāzē</string>
<string name="label_network">Tīkls</string>
<string name="label_data">Dati</string>
<string name="used_cache">Izmantots: %1$s</string>
<string name="clear_database_confirmation">Vai esi pārliecināts\? Lasītās nodaļas un progress priekš manga, kuras nav bibliotēkā, būs zudis</string>
<string name="database_clean">Datu bāze tīra</string>
<string name="pref_disable_battery_optimization">Atspējot akumulatora optimizāciju</string>
<string name="pref_enable_automatic_extension_updates">Pārbaudīt, vai nav paplašinājumu atjauninājumi</string>
<string name="pref_restore_backup_summ">Atjaunot bibliotēku no dublējuma faila</string>
<string name="pref_backup_slots">Maksimālais dublējumu skaits</string>
<string name="pref_clear_cookies">Notīrīt sīkfailus</string>
<string name="pref_dns_over_https">Izvēlēties DNS pār HTTPS (DoH)</string>
<string name="cookies_cleared">Sīkfaili notīrīti</string>
<string name="cache_delete_error">Tīrīšanas laikā radās kļūda</string>
<string name="pref_clear_database">Notīrīt datu bāzi</string>
<string name="pref_clear_database_summary">Dzēst vēsturi preikš manga, kas nav saglabāta bibliotēkā</string>
<string name="pref_refresh_library_tracking_summary">Atjaunina statusu, vērtējumu un pēdējo izlasīto nodaļu no sekošanas servisa</string>
<string name="pref_reset_viewer_flags">Atiestatīt atsevišķu sēriju lasītāja iestatījumus</string>
<string name="pref_reset_viewer_flags_summary">Atiestatīt katras sērijas lasīšanas režīmu un orientāciju</string>
<string name="pref_tablet_ui_mode">Planšetdatora lietotāja interfeiss</string>
<string name="pref_verbose_logging">Verbose reģistrēšana</string>
<string name="website">Tīmekļa vietne</string>
<string name="version">Versija</string>
<string name="whats_new">Jaunumi</string>
<string name="updated_version">Atjaunināts uz v%1$s</string>
<string name="licenses">Atvērtā avota licences</string>
<string name="check_for_updates">Pārbaudīt, vai nav atjauninājumu</string>
<string name="pref_clear_webview_data">Notīrīt WebView datus</string>
<string name="webview_data_deleted">WebView dati notīrīti</string>
<string name="crash_log_saved">Avārijas žurnāli saglabāti</string>
<string name="label_background_activity">Fona darbība</string>
<string name="battery_optimization_setting_activity_not_found">Nevarēja atvērt ierīces iestatījumus</string>
<string name="services">Serviss</string>
<string name="backup_created">Dublējums izveidots</string>
<string name="pref_refresh_library_tracking">Atsvaidzināt sekošanu</string>
<string name="pref_reset_viewer_flags_success">Visi lasītāja iestatījumi atiestatīti</string>
<string name="pref_reset_viewer_flags_error">Nevarēja atiestatīt lasītāja iestatījumus</string>
<string name="requires_app_restart">Lai stātos spēkā, ir nepieciešama lietotnes restartēšana</string>
<string name="action_track">Sekot</string>
<string name="enhanced_services">Uzlabotie pakalpojumi</string>
<string name="tracker_not_logged_in">Nav pieteicies: %1$s</string>
<string name="backup_restore_invalid_uri">Kļūda: tukšs URI</string>
<string name="help_translate">Palīdzi tulkot</string>
<string name="pref_enable_acra">Sūtīt avārijas ziņojumus</string>
<string name="cache_deleted">Kešatmiņa nodzēsta. %1$d faili ir izdzēsti</string>
</resources>

View file

@ -21,6 +21,10 @@ delete:
DELETE FROM manga_sync
WHERE manga_id = :mangaId AND sync_id = :syncId;
getTracks:
SELECT *
FROM manga_sync;
getTracksByMangaId:
SELECT *
FROM manga_sync

View file

@ -86,6 +86,61 @@ AND C.date_upload > :after
AND C.date_fetch > M.date_added
ORDER BY C.date_upload DESC;
getLibrary:
SELECT M.*, COALESCE(MC.category_id, 0) AS category
FROM (
SELECT mangas.*, COALESCE(C.unreadCount, 0) AS unread_count, COALESCE(R.readCount, 0) AS read_count
FROM mangas
LEFT JOIN (
SELECT chapters.manga_id, COUNT(*) AS unreadCount
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.manga_id
) AS C
ON mangas._id = C.manga_id
LEFT JOIN (
SELECT chapters.manga_id, COUNT(*) AS readCount
FROM chapters
WHERE chapters.read = 1
GROUP BY chapters.manga_id
) AS R
WHERE mangas.favorite = 1
GROUP BY mangas._id
ORDER BY mangas.title
) AS M
LEFT JOIN (
SELECT *
FROM mangas_categories
) AS MC
ON M._id = MC.manga_id;
getLastRead:
SELECT M.*, MAX(H.last_read) AS max
FROM mangas M
JOIN chapters C
ON M._id = C.manga_id
JOIN history H
ON C._id = H.chapter_id
WHERE M.favorite = 1
GROUP BY M._id
ORDER BY max ASC;
getLatestByChapterUploadDate:
SELECT M.*, MAX(C.date_upload) AS max
FROM mangas M
JOIN chapters C
ON M._id = C.manga_id
GROUP BY M._id
ORDER BY max ASC;
getLatestByChapterFetchDate:
SELECT M.*, MAX(C.date_fetch) AS max
FROM mangas M
JOIN chapters C
ON M._id = C.manga_id
GROUP BY M._id
ORDER BY max ASC;
deleteMangasNotInLibraryBySourceIds:
DELETE FROM mangas
WHERE favorite = 0 AND source IN :sourceIds;

View file

@ -21,6 +21,9 @@ delete:
DELETE FROM anime_sync
WHERE anime_id = :animeId AND sync_id = :syncId;
getAnimeTracks:
SELECT *
FROM anime_sync;
getTracksByAnimeId:
SELECT *

View file

@ -86,6 +86,62 @@ AND EP.date_upload > :after
AND EP.date_fetch > A.date_added
ORDER BY EP.date_upload DESC;
getAnimelib:
SELECT M.*, COALESCE(MC.category_id, 0) AS category
FROM (
SELECT animes.*, COALESCE(C.unseenCount, 0) AS unseen_count, COALESCE(R.seenCount, 0) AS seen_count
FROM animes
LEFT JOIN (
SELECT episodes.anime_id, COUNT(*) AS unseenCount
FROM episodes
WHERE episodes.seen = 0
GROUP BY episodes.anime_id
) AS C
ON animes._id = C.anime_id
LEFT JOIN (
SELECT episodes.anime_id, COUNT(*) AS seenCount
FROM episodes
WHERE episodes.seen = 1
GROUP BY episodes.anime_id
) AS R
WHERE animes.favorite = 1
GROUP BY animes._id
ORDER BY animes.title
) AS M
LEFT JOIN (
SELECT *
FROM animes_categories
) AS MC
ON M._id = MC.anime_id;
getLastSeen:
SELECT M.*, MAX(H.last_seen) AS max
FROM animes M
JOIN episodes C
ON M._id = C.anime_id
JOIN animehistory H
ON C._id = H.episode_id
WHERE M.favorite = 1
GROUP BY M._id
ORDER BY max ASC;
getLatestByEpisodeUploadDate:
SELECT M.*, MAX(C.date_upload) AS max
FROM animes M
JOIN episodes C
ON M._id = C.anime_id
GROUP BY M._id
ORDER BY max ASC;
getLatestByEpisodeFetchDate:
SELECT M.*, MAX(C.date_fetch) AS max
FROM animes M
JOIN episodes C
ON M._id = C.anime_id
GROUP BY M._id
ORDER BY max ASC;
deleteAnimesNotInLibraryBySourceIds:
DELETE FROM animes
WHERE favorite = 0 AND source IN :sourceIds;