mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Database queries are now separated by table. Improve how the app creates downloads
This commit is contained in:
		@@ -1,26 +1,17 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.Pair
 | 
			
		||||
import com.pushtorefresh.storio.Queries
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.Query
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.*
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.*
 | 
			
		||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.base.Source
 | 
			
		||||
import eu.kanade.tachiyomi.util.ChapterRecognition
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import java.util.*
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.queries.*
 | 
			
		||||
 | 
			
		||||
open class DatabaseHelper(context: Context) {
 | 
			
		||||
/**
 | 
			
		||||
 * This class provides operations to manage the database through its interfaces.
 | 
			
		||||
 */
 | 
			
		||||
open class DatabaseHelper(context: Context)
 | 
			
		||||
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries {
 | 
			
		||||
 | 
			
		||||
    val db = DefaultStorIOSQLite.builder()
 | 
			
		||||
    override val db = DefaultStorIOSQLite.builder()
 | 
			
		||||
            .sqliteOpenHelper(DbOpenHelper(context))
 | 
			
		||||
            .addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
 | 
			
		||||
            .addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
 | 
			
		||||
@@ -29,287 +20,6 @@ open class DatabaseHelper(context: Context) {
 | 
			
		||||
            .addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    inline fun inTransaction(func: DatabaseHelper.() -> Unit) {
 | 
			
		||||
        db.internal().beginTransaction()
 | 
			
		||||
        try {
 | 
			
		||||
            func()
 | 
			
		||||
            db.internal().setTransactionSuccessful()
 | 
			
		||||
        } finally {
 | 
			
		||||
            db.internal().endTransaction()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Mangas related queries
 | 
			
		||||
 | 
			
		||||
    fun getMangas() = db.get()
 | 
			
		||||
            .listOfObjects(Manga::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getLibraryMangas() = db.get()
 | 
			
		||||
            .listOfObjects(Manga::class.java)
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query(libraryQuery)
 | 
			
		||||
                    .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .withGetResolver(LibraryMangaGetResolver.INSTANCE)
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    open fun getFavoriteMangas() = db.get()
 | 
			
		||||
            .listOfObjects(Manga::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .where("${MangaTable.COLUMN_FAVORITE} = ?")
 | 
			
		||||
                    .whereArgs(1)
 | 
			
		||||
                    .orderBy(MangaTable.COLUMN_TITLE)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getManga(url: String, sourceId: Int) = db.get()
 | 
			
		||||
            .`object`(Manga::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .where("${MangaTable.COLUMN_URL} = ? AND ${MangaTable.COLUMN_SOURCE} = ?")
 | 
			
		||||
                    .whereArgs(url, sourceId)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getManga(id: Long) = db.get()
 | 
			
		||||
            .`object`(Manga::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .where("${MangaTable.COLUMN_ID} = ?")
 | 
			
		||||
                    .whereArgs(id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangasNotInLibrary() = db.delete()
 | 
			
		||||
            .byQuery(DeleteQuery.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .where("${MangaTable.COLUMN_FAVORITE} = ?")
 | 
			
		||||
                    .whereArgs(0)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Chapters related queries
 | 
			
		||||
 | 
			
		||||
    fun getChapters(manga: Manga) = db.get()
 | 
			
		||||
            .listOfObjects(Chapter::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(ChapterTable.TABLE)
 | 
			
		||||
                    .where("${ChapterTable.COLUMN_MANGA_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getRecentChapters(date: Date) = db.get()
 | 
			
		||||
            .listOfObjects(MangaChapter::class.java)
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query(getRecentsQuery(date))
 | 
			
		||||
                    .observesTables(ChapterTable.TABLE)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .withGetResolver(MangaChapterGetResolver.INSTANCE)
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
 | 
			
		||||
        // Add a delta to the chapter number, because binary decimal representation
 | 
			
		||||
        // can retrieve the same chapter again
 | 
			
		||||
        val chapterNumber = chapter.chapter_number + 0.00001
 | 
			
		||||
 | 
			
		||||
        return db.get()
 | 
			
		||||
                .`object`(Chapter::class.java)
 | 
			
		||||
                .withQuery(Query.builder()
 | 
			
		||||
                        .table(ChapterTable.TABLE)
 | 
			
		||||
                        .where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
 | 
			
		||||
                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} > ? AND " +
 | 
			
		||||
                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} <= ?")
 | 
			
		||||
                        .whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
 | 
			
		||||
                        .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
 | 
			
		||||
                        .limit(1)
 | 
			
		||||
                        .build())
 | 
			
		||||
                .prepare()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getPreviousChapter(chapter: Chapter): PreparedGetObject<Chapter> {
 | 
			
		||||
        // Add a delta to the chapter number, because binary decimal representation
 | 
			
		||||
        // can retrieve the same chapter again
 | 
			
		||||
        val chapterNumber = chapter.chapter_number - 0.00001
 | 
			
		||||
 | 
			
		||||
        return db.get()
 | 
			
		||||
                .`object`(Chapter::class.java)
 | 
			
		||||
                .withQuery(Query.builder().table(ChapterTable.TABLE)
 | 
			
		||||
                        .where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
 | 
			
		||||
                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} < ? AND " +
 | 
			
		||||
                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
 | 
			
		||||
                        .whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
 | 
			
		||||
                        .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
 | 
			
		||||
                        .limit(1)
 | 
			
		||||
                        .build())
 | 
			
		||||
                .prepare()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getNextUnreadChapter(manga: Manga) = db.get()
 | 
			
		||||
            .`object`(Chapter::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(ChapterTable.TABLE)
 | 
			
		||||
                    .where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
 | 
			
		||||
                            "${ChapterTable.COLUMN_READ} = ? AND " +
 | 
			
		||||
                            "${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
 | 
			
		||||
                    .whereArgs(manga.id, 0, 0)
 | 
			
		||||
                    .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
 | 
			
		||||
                    .limit(1)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
 | 
			
		||||
 | 
			
		||||
    // Add new chapters or delete if the source deletes them
 | 
			
		||||
    open fun insertOrRemoveChapters(manga: Manga, sourceChapters: List<Chapter>, source: Source): Observable<Pair<Int, Int>> {
 | 
			
		||||
        val dbChapters = getChapters(manga).executeAsBlocking()
 | 
			
		||||
 | 
			
		||||
        val newChapters = Observable.from(sourceChapters)
 | 
			
		||||
                .filter { it !in dbChapters }
 | 
			
		||||
                .doOnNext { c ->
 | 
			
		||||
                    c.manga_id = manga.id
 | 
			
		||||
                    source.parseChapterNumber(c)
 | 
			
		||||
                    ChapterRecognition.parseChapterNumber(c, manga)
 | 
			
		||||
                }.toList()
 | 
			
		||||
 | 
			
		||||
        val deletedChapters = Observable.from(dbChapters)
 | 
			
		||||
                .filter { it !in sourceChapters }
 | 
			
		||||
                .toList()
 | 
			
		||||
 | 
			
		||||
        return Observable.zip(newChapters, deletedChapters) { toAdd, toDelete ->
 | 
			
		||||
            var added = 0
 | 
			
		||||
            var deleted = 0
 | 
			
		||||
            var readded = 0
 | 
			
		||||
 | 
			
		||||
            inTransaction {
 | 
			
		||||
                val deletedReadChapterNumbers = TreeSet<Float>()
 | 
			
		||||
                if (!toDelete.isEmpty()) {
 | 
			
		||||
                    for (c in toDelete) {
 | 
			
		||||
                        if (c.read) {
 | 
			
		||||
                            deletedReadChapterNumbers.add(c.chapter_number)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    deleted = deleteChapters(toDelete).executeAsBlocking().results().size
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!toAdd.isEmpty()) {
 | 
			
		||||
                    // Set the date fetch for new items in reverse order to allow another sorting method.
 | 
			
		||||
                    // Sources MUST return the chapters from most to less recent, which is common.
 | 
			
		||||
                    var now = Date().time
 | 
			
		||||
 | 
			
		||||
                    for (i in toAdd.indices.reversed()) {
 | 
			
		||||
                        val c = toAdd[i]
 | 
			
		||||
                        c.date_fetch = now++
 | 
			
		||||
                        // Try to mark already read chapters as read when the source deletes them
 | 
			
		||||
                        if (c.chapter_number != -1f && c.chapter_number in deletedReadChapterNumbers) {
 | 
			
		||||
                            c.read = true
 | 
			
		||||
                            readded++
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    added = insertChapters(toAdd).executeAsBlocking().numberOfInserts()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Pair.create(added - readded, deleted - readded)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
 | 
			
		||||
 | 
			
		||||
    // Manga sync related queries
 | 
			
		||||
 | 
			
		||||
    fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
 | 
			
		||||
            .`object`(MangaSync::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaSyncTable.TABLE)
 | 
			
		||||
                    .where("${MangaSyncTable.COLUMN_MANGA_ID} = ? AND " +
 | 
			
		||||
                            "${MangaSyncTable.COLUMN_SYNC_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id, sync.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getMangasSync(manga: Manga) = db.get()
 | 
			
		||||
            .listOfObjects(MangaSync::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaSyncTable.TABLE)
 | 
			
		||||
                    .where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangaSyncForManga(manga: Manga) = db.delete()
 | 
			
		||||
            .byQuery(DeleteQuery.builder()
 | 
			
		||||
                    .table(MangaSyncTable.TABLE)
 | 
			
		||||
                    .where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    // Categories related queries
 | 
			
		||||
 | 
			
		||||
    fun getCategories() = db.get()
 | 
			
		||||
            .listOfObjects(Category::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(CategoryTable.TABLE)
 | 
			
		||||
                    .orderBy(CategoryTable.COLUMN_ORDER)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getCategoriesForManga(manga: Manga) = db.get()
 | 
			
		||||
            .listOfObjects(Category::class.java)
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query(getCategoriesForMangaQuery(manga))
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertCategory(category: Category) = db.put().`object`(category).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
 | 
			
		||||
            .byQuery(DeleteQuery.builder()
 | 
			
		||||
                    .table(MangaCategoryTable.TABLE)
 | 
			
		||||
                    .where("${MangaCategoryTable.COLUMN_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
 | 
			
		||||
                    .whereArgs(*mangas.map { it.id }.toTypedArray())
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
 | 
			
		||||
        inTransaction {
 | 
			
		||||
            deleteOldMangasCategories(mangas).executeAsBlocking()
 | 
			
		||||
            insertMangasCategories(mangasCategories).executeAsBlocking()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database
 | 
			
		||||
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
 | 
			
		||||
 | 
			
		||||
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
 | 
			
		||||
    internal().beginTransaction()
 | 
			
		||||
    try {
 | 
			
		||||
        block()
 | 
			
		||||
        internal().setTransactionSuccessful()
 | 
			
		||||
    } finally {
 | 
			
		||||
        internal().endTransaction()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
 | 
			
		||||
    internal().beginTransaction()
 | 
			
		||||
    try {
 | 
			
		||||
        val result = block()
 | 
			
		||||
        internal().setTransactionSuccessful()
 | 
			
		||||
        return result
 | 
			
		||||
    } finally {
 | 
			
		||||
        internal().endTransaction()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database
 | 
			
		||||
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
 | 
			
		||||
 | 
			
		||||
interface DbProvider {
 | 
			
		||||
 | 
			
		||||
    val db: DefaultStorIOSQLite
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
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.Category
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
 | 
			
		||||
 | 
			
		||||
interface CategoryQueries : DbProvider {
 | 
			
		||||
 | 
			
		||||
    fun getCategories() = db.get()
 | 
			
		||||
            .listOfObjects(Category::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(CategoryTable.TABLE)
 | 
			
		||||
                    .orderBy(CategoryTable.COLUMN_ORDER)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getCategoriesForManga(manga: Manga) = db.get()
 | 
			
		||||
            .listOfObjects(Category::class.java)
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query(getCategoriesForMangaQuery())
 | 
			
		||||
                    .args(manga.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertCategory(category: Category) = db.put().`object`(category).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,158 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database.queries
 | 
			
		||||
 | 
			
		||||
import android.util.Pair
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
 | 
			
		||||
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.inTransaction
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.base.Source
 | 
			
		||||
import eu.kanade.tachiyomi.util.ChapterRecognition
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
interface ChapterQueries : DbProvider {
 | 
			
		||||
 | 
			
		||||
    fun getChapters(manga: Manga) = db.get()
 | 
			
		||||
            .listOfObjects(Chapter::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(ChapterTable.TABLE)
 | 
			
		||||
                    .where("${ChapterTable.COLUMN_MANGA_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getRecentChapters(date: Date) = db.get()
 | 
			
		||||
            .listOfObjects(MangaChapter::class.java)
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query(getRecentsQuery())
 | 
			
		||||
                    .args(date.time)
 | 
			
		||||
                    .observesTables(ChapterTable.TABLE)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .withGetResolver(MangaChapterGetResolver.INSTANCE)
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
 | 
			
		||||
        // Add a delta to the chapter number, because binary decimal representation
 | 
			
		||||
        // can retrieve the same chapter again
 | 
			
		||||
        val chapterNumber = chapter.chapter_number + 0.00001
 | 
			
		||||
 | 
			
		||||
        return db.get()
 | 
			
		||||
                .`object`(Chapter::class.java)
 | 
			
		||||
                .withQuery(Query.builder()
 | 
			
		||||
                        .table(ChapterTable.TABLE)
 | 
			
		||||
                        .where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
 | 
			
		||||
                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} > ? AND " +
 | 
			
		||||
                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} <= ?")
 | 
			
		||||
                        .whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
 | 
			
		||||
                        .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
 | 
			
		||||
                        .limit(1)
 | 
			
		||||
                        .build())
 | 
			
		||||
                .prepare()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getPreviousChapter(chapter: Chapter): PreparedGetObject<Chapter> {
 | 
			
		||||
        // Add a delta to the chapter number, because binary decimal representation
 | 
			
		||||
        // can retrieve the same chapter again
 | 
			
		||||
        val chapterNumber = chapter.chapter_number - 0.00001
 | 
			
		||||
 | 
			
		||||
        return db.get()
 | 
			
		||||
                .`object`(Chapter::class.java)
 | 
			
		||||
                .withQuery(Query.builder().table(ChapterTable.TABLE)
 | 
			
		||||
                        .where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
 | 
			
		||||
                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} < ? AND " +
 | 
			
		||||
                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
 | 
			
		||||
                        .whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
 | 
			
		||||
                        .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
 | 
			
		||||
                        .limit(1)
 | 
			
		||||
                        .build())
 | 
			
		||||
                .prepare()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getNextUnreadChapter(manga: Manga) = db.get()
 | 
			
		||||
            .`object`(Chapter::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(ChapterTable.TABLE)
 | 
			
		||||
                    .where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
 | 
			
		||||
                            "${ChapterTable.COLUMN_READ} = ? AND " +
 | 
			
		||||
                            "${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
 | 
			
		||||
                    .whereArgs(manga.id, 0, 0)
 | 
			
		||||
                    .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
 | 
			
		||||
                    .limit(1)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
 | 
			
		||||
 | 
			
		||||
    // TODO this logic shouldn't be here
 | 
			
		||||
    // Add new chapters or delete if the source deletes them
 | 
			
		||||
    open fun insertOrRemoveChapters(manga: Manga, sourceChapters: List<Chapter>, source: Source): Observable<Pair<Int, Int>> {
 | 
			
		||||
        val dbChapters = getChapters(manga).executeAsBlocking()
 | 
			
		||||
 | 
			
		||||
        val newChapters = Observable.from(sourceChapters)
 | 
			
		||||
                .filter { it !in dbChapters }
 | 
			
		||||
                .doOnNext { c ->
 | 
			
		||||
                    c.manga_id = manga.id
 | 
			
		||||
                    source.parseChapterNumber(c)
 | 
			
		||||
                    ChapterRecognition.parseChapterNumber(c, manga)
 | 
			
		||||
                }.toList()
 | 
			
		||||
 | 
			
		||||
        val deletedChapters = Observable.from(dbChapters)
 | 
			
		||||
                .filter { it !in sourceChapters }
 | 
			
		||||
                .toList()
 | 
			
		||||
 | 
			
		||||
        return Observable.zip(newChapters, deletedChapters) { toAdd, toDelete ->
 | 
			
		||||
            var added = 0
 | 
			
		||||
            var deleted = 0
 | 
			
		||||
            var readded = 0
 | 
			
		||||
 | 
			
		||||
            db.inTransaction {
 | 
			
		||||
                val deletedReadChapterNumbers = TreeSet<Float>()
 | 
			
		||||
                if (!toDelete.isEmpty()) {
 | 
			
		||||
                    for (c in toDelete) {
 | 
			
		||||
                        if (c.read) {
 | 
			
		||||
                            deletedReadChapterNumbers.add(c.chapter_number)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    deleted = deleteChapters(toDelete).executeAsBlocking().results().size
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!toAdd.isEmpty()) {
 | 
			
		||||
                    // Set the date fetch for new items in reverse order to allow another sorting method.
 | 
			
		||||
                    // Sources MUST return the chapters from most to less recent, which is common.
 | 
			
		||||
                    var now = Date().time
 | 
			
		||||
 | 
			
		||||
                    for (i in toAdd.indices.reversed()) {
 | 
			
		||||
                        val c = toAdd[i]
 | 
			
		||||
                        c.date_fetch = now++
 | 
			
		||||
                        // Try to mark already read chapters as read when the source deletes them
 | 
			
		||||
                        if (c.chapter_number != -1f && c.chapter_number in deletedReadChapterNumbers) {
 | 
			
		||||
                            c.read = true
 | 
			
		||||
                            readded++
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    added = insertChapters(toAdd).executeAsBlocking().numberOfInserts()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Pair.create(added - readded, deleted - readded)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
 | 
			
		||||
 | 
			
		||||
    fun updateChapterProgress(chapter: Chapter) = db.put()
 | 
			
		||||
            .`object`(chapter)
 | 
			
		||||
            .withPutResolver(ChapterProgressPutResolver.instance)
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database.queries
 | 
			
		||||
 | 
			
		||||
import com.pushtorefresh.storio.Queries
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DbProvider
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.inTransaction
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
 | 
			
		||||
 | 
			
		||||
interface MangaCategoryQueries : DbProvider {
 | 
			
		||||
 | 
			
		||||
    fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
 | 
			
		||||
            .byQuery(DeleteQuery.builder()
 | 
			
		||||
                    .table(MangaCategoryTable.TABLE)
 | 
			
		||||
                    .where("${MangaCategoryTable.COLUMN_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
 | 
			
		||||
                    .whereArgs(*mangas.map { it.id }.toTypedArray())
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
 | 
			
		||||
        db.inTransaction {
 | 
			
		||||
            deleteOldMangasCategories(mangas).executeAsBlocking()
 | 
			
		||||
            insertMangasCategories(mangasCategories).executeAsBlocking()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,75 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database.queries
 | 
			
		||||
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
 | 
			
		||||
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.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
 | 
			
		||||
 | 
			
		||||
interface MangaQueries : DbProvider {
 | 
			
		||||
 | 
			
		||||
    fun getMangas() = db.get()
 | 
			
		||||
            .listOfObjects(Manga::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getLibraryMangas() = db.get()
 | 
			
		||||
            .listOfObjects(Manga::class.java)
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query(libraryQuery)
 | 
			
		||||
                    .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .withGetResolver(LibraryMangaGetResolver.INSTANCE)
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    open fun getFavoriteMangas() = db.get()
 | 
			
		||||
            .listOfObjects(Manga::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .where("${MangaTable.COLUMN_FAVORITE} = ?")
 | 
			
		||||
                    .whereArgs(1)
 | 
			
		||||
                    .orderBy(MangaTable.COLUMN_TITLE)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getManga(url: String, sourceId: Int) = db.get()
 | 
			
		||||
            .`object`(Manga::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .where("${MangaTable.COLUMN_URL} = ? AND ${MangaTable.COLUMN_SOURCE} = ?")
 | 
			
		||||
                    .whereArgs(url, sourceId)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getManga(id: Long) = db.get()
 | 
			
		||||
            .`object`(Manga::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .where("${MangaTable.COLUMN_ID} = ?")
 | 
			
		||||
                    .whereArgs(id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangasNotInLibrary() = db.delete()
 | 
			
		||||
            .byQuery(DeleteQuery.builder()
 | 
			
		||||
                    .table(MangaTable.TABLE)
 | 
			
		||||
                    .where("${MangaTable.COLUMN_FAVORITE} = ?")
 | 
			
		||||
                    .whereArgs(0)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database.queries
 | 
			
		||||
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.Query
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DbProvider
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
 | 
			
		||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
 | 
			
		||||
 | 
			
		||||
interface MangaSyncQueries : DbProvider {
 | 
			
		||||
 | 
			
		||||
    fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
 | 
			
		||||
            .`object`(MangaSync::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaSyncTable.TABLE)
 | 
			
		||||
                    .where("${MangaSyncTable.COLUMN_MANGA_ID} = ? AND " +
 | 
			
		||||
                            "${MangaSyncTable.COLUMN_SYNC_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id, sync.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getMangasSync(manga: Manga) = db.get()
 | 
			
		||||
            .listOfObjects(MangaSync::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaSyncTable.TABLE)
 | 
			
		||||
                    .where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangaSyncForManga(manga: Manga) = db.delete()
 | 
			
		||||
            .byQuery(DeleteQuery.builder()
 | 
			
		||||
                    .table(MangaSyncTable.TABLE)
 | 
			
		||||
                    .where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database
 | 
			
		||||
package eu.kanade.tachiyomi.data.database.queries
 | 
			
		||||
 | 
			
		||||
import java.util.*
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga as MangaModel
 | 
			
		||||
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.MangaCategoryTable as MangaCategory
 | 
			
		||||
@@ -32,23 +30,19 @@ val libraryQuery =
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Query to get the recent chapters of manga from the library up to a date.
 | 
			
		||||
 *
 | 
			
		||||
 * @param date the delimiting date.
 | 
			
		||||
 */
 | 
			
		||||
fun getRecentsQuery(date: Date): String =
 | 
			
		||||
fun getRecentsQuery() =
 | 
			
		||||
    "SELECT ${Manga.TABLE}.${Manga.COLUMN_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} " +
 | 
			
		||||
    "ON ${Manga.TABLE}.${Manga.COLUMN_ID} = ${Chapter.TABLE}.${Chapter.COLUMN_MANGA_ID} " +
 | 
			
		||||
    "WHERE ${Manga.COLUMN_FAVORITE} = 1 AND ${Chapter.COLUMN_DATE_UPLOAD} > ${date.time} " +
 | 
			
		||||
    "WHERE ${Manga.COLUMN_FAVORITE} = 1 AND ${Chapter.COLUMN_DATE_UPLOAD} > ? " +
 | 
			
		||||
    "ORDER BY ${Chapter.COLUMN_DATE_UPLOAD} DESC"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Query to get the categorias for a manga.
 | 
			
		||||
 *
 | 
			
		||||
 * @param manga the manga.
 | 
			
		||||
 * Query to get the categories for a manga.
 | 
			
		||||
 */
 | 
			
		||||
fun getCategoriesForMangaQuery(manga: MangaModel) =
 | 
			
		||||
fun getCategoriesForMangaQuery() =
 | 
			
		||||
    "SELECT ${Category.TABLE}.* FROM ${Category.TABLE} " +
 | 
			
		||||
    "JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COLUMN_ID} = " +
 | 
			
		||||
    "${MangaCategory.TABLE}.${MangaCategory.COLUMN_CATEGORY_ID} " +
 | 
			
		||||
    "WHERE ${MangaCategory.COLUMN_MANGA_ID} = ${manga.id}"
 | 
			
		||||
    "WHERE ${MangaCategory.COLUMN_MANGA_ID} = ?"
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database.resolvers
 | 
			
		||||
 | 
			
		||||
import android.content.ContentValues
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
 | 
			
		||||
 | 
			
		||||
class ChapterProgressPutResolver : PutResolver<Chapter>() {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        val instance = ChapterProgressPutResolver()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
 | 
			
		||||
        val updateQuery = mapToUpdateQuery(chapter)
 | 
			
		||||
        val contentValues = mapToContentValues(chapter)
 | 
			
		||||
 | 
			
		||||
        val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
 | 
			
		||||
        PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
 | 
			
		||||
            .table(ChapterTable.TABLE)
 | 
			
		||||
            .where("${ChapterTable.COLUMN_ID} = ?")
 | 
			
		||||
            .whereArgs(chapter.id)
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    fun mapToContentValues(chapter: Chapter) = ContentValues(2).apply {
 | 
			
		||||
        put(ChapterTable.COLUMN_READ, chapter.read)
 | 
			
		||||
        put(ChapterTable.COLUMN_LAST_PAGE_READ, chapter.last_page_read)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.base.Source
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
 | 
			
		||||
import eu.kanade.tachiyomi.util.*
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
@@ -45,7 +44,8 @@ class DownloadManager(private val context: Context, private val sourceManager: S
 | 
			
		||||
 | 
			
		||||
    val PAGE_LIST_FILE = "index.json"
 | 
			
		||||
 | 
			
		||||
    @Volatile private var isRunning: Boolean = false
 | 
			
		||||
    @Volatile var isRunning: Boolean = false
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    private fun initializeSubscriptions() {
 | 
			
		||||
        downloadsSubscription?.unsubscribe()
 | 
			
		||||
@@ -91,16 +91,15 @@ class DownloadManager(private val context: Context, private val sourceManager: S
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create a download object for every chapter in the event and add them to the downloads queue
 | 
			
		||||
    fun onDownloadChaptersEvent(event: DownloadChaptersEvent) {
 | 
			
		||||
        val manga = event.manga
 | 
			
		||||
    // Create a download object for every chapter and add them to the downloads queue
 | 
			
		||||
    fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
 | 
			
		||||
        val source = sourceManager.get(manga.source)
 | 
			
		||||
 | 
			
		||||
        // Used to avoid downloading chapters with the same name
 | 
			
		||||
        val addedChapters = ArrayList<String>()
 | 
			
		||||
        val pending = ArrayList<Download>()
 | 
			
		||||
 | 
			
		||||
        for (chapter in event.chapters) {
 | 
			
		||||
        for (chapter in chapters) {
 | 
			
		||||
            if (addedChapters.contains(chapter.name))
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.event
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
 | 
			
		||||
class DownloadChaptersEvent(val manga: Manga, val chapters: List<Chapter>)
 | 
			
		||||
@@ -4,6 +4,7 @@ import android.animation.Animator
 | 
			
		||||
import android.animation.AnimatorListenerAdapter
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v4.app.DialogFragment
 | 
			
		||||
import android.support.v7.view.ActionMode
 | 
			
		||||
import android.view.*
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
@@ -11,7 +12,6 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadService
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.decoration.DividerItemDecoration
 | 
			
		||||
@@ -21,12 +21,11 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.getCoordinates
 | 
			
		||||
import eu.kanade.tachiyomi.util.getResourceDrawable
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
 | 
			
		||||
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_manga_chapters.*
 | 
			
		||||
import nucleus.factory.RequiresPresenter
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
 | 
			
		||||
@RequiresPresenter(ChaptersPresenter::class)
 | 
			
		||||
class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
 | 
			
		||||
@@ -40,6 +39,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
        fun newInstance(): ChaptersFragment {
 | 
			
		||||
            return ChaptersFragment()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -73,7 +73,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
 | 
			
		||||
        swipe_refresh.setOnRefreshListener { fetchChapters() }
 | 
			
		||||
 | 
			
		||||
        fab.setOnClickListener { v ->
 | 
			
		||||
        fab.setOnClickListener {
 | 
			
		||||
            val chapter = presenter.getNextUnreadChapter()
 | 
			
		||||
            if (chapter != null) {
 | 
			
		||||
                // Create animation listener
 | 
			
		||||
@@ -252,7 +252,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
                                    chapters = chapters.subList(0, 10)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        onDownload(Observable.from(chapters))
 | 
			
		||||
                        downloadChapters(chapters)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .show()
 | 
			
		||||
@@ -278,11 +278,11 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
 | 
			
		||||
    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.action_select_all -> onSelectAll()
 | 
			
		||||
            R.id.action_mark_as_read -> onMarkAsRead(getSelectedChapters())
 | 
			
		||||
            R.id.action_mark_as_unread -> onMarkAsUnread(getSelectedChapters())
 | 
			
		||||
            R.id.action_download -> onDownload(getSelectedChapters())
 | 
			
		||||
            R.id.action_delete -> onDelete(getSelectedChapters())
 | 
			
		||||
            R.id.action_select_all -> selectAll()
 | 
			
		||||
            R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
 | 
			
		||||
            R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
 | 
			
		||||
            R.id.action_download -> downloadChapters(getSelectedChapters())
 | 
			
		||||
            R.id.action_delete -> deleteChapters(getSelectedChapters())
 | 
			
		||||
            else -> return false
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
@@ -294,66 +294,57 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
        actionMode = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getSelectedChapters(): Observable<Chapter> {
 | 
			
		||||
        val chapters = adapter.selectedItems.map { adapter.getItem(it) }
 | 
			
		||||
        return Observable.from(chapters)
 | 
			
		||||
    fun getSelectedChapters(): List<Chapter> {
 | 
			
		||||
        return adapter.selectedItems.map { adapter.getItem(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun destroyActionModeIfNeeded() {
 | 
			
		||||
        actionMode?.finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected fun onSelectAll() {
 | 
			
		||||
    fun selectAll() {
 | 
			
		||||
        adapter.selectAll()
 | 
			
		||||
        setContextTitle(adapter.selectedItemCount)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onMarkAsRead(chapters: Observable<Chapter>) {
 | 
			
		||||
    fun markAsRead(chapters: List<Chapter>) {
 | 
			
		||||
        presenter.markChaptersRead(chapters, true)
 | 
			
		||||
        if (presenter.preferences.removeAfterMarkedAsRead()) {
 | 
			
		||||
            deleteChapters(chapters)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onMarkAsUnread(chapters: Observable<Chapter>) {
 | 
			
		||||
    fun markAsUnread(chapters: List<Chapter>) {
 | 
			
		||||
        presenter.markChaptersRead(chapters, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onMarkPreviousAsRead(chapter: Chapter) {
 | 
			
		||||
    fun markPreviousAsRead(chapter: Chapter) {
 | 
			
		||||
        presenter.markPreviousChaptersAsRead(chapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onDownload(chapters: Observable<Chapter>) {
 | 
			
		||||
        DownloadService.start(activity)
 | 
			
		||||
 | 
			
		||||
        val observable = chapters.doOnCompleted { adapter.notifyDataSetChanged() }
 | 
			
		||||
 | 
			
		||||
        presenter.downloadChapters(observable)
 | 
			
		||||
    fun downloadChapters(chapters: List<Chapter>) {
 | 
			
		||||
        destroyActionModeIfNeeded()
 | 
			
		||||
        presenter.downloadChapters(chapters)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onDelete(chapters: Observable<Chapter>) {
 | 
			
		||||
        val size = adapter.selectedItemCount
 | 
			
		||||
 | 
			
		||||
        val dialog = MaterialDialog.Builder(activity)
 | 
			
		||||
                .title(R.string.deleting)
 | 
			
		||||
                .progress(false, size, true)
 | 
			
		||||
                .cancelable(false)
 | 
			
		||||
                .show()
 | 
			
		||||
 | 
			
		||||
        val observable = chapters
 | 
			
		||||
                .concatMap { chapter ->
 | 
			
		||||
                    presenter.deleteChapter(chapter)
 | 
			
		||||
                    Observable.just(chapter)
 | 
			
		||||
                }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .doOnNext { chapter ->
 | 
			
		||||
                    dialog.incrementProgress(1)
 | 
			
		||||
                    chapter.status = Download.NOT_DOWNLOADED
 | 
			
		||||
                }
 | 
			
		||||
                .doOnCompleted { adapter.notifyDataSetChanged() }
 | 
			
		||||
                .doAfterTerminate { dialog.dismiss() }
 | 
			
		||||
 | 
			
		||||
        presenter.deleteChapters(observable)
 | 
			
		||||
    fun deleteChapters(chapters: List<Chapter>) {
 | 
			
		||||
        destroyActionModeIfNeeded()
 | 
			
		||||
        DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
 | 
			
		||||
        presenter.deleteChapters(chapters)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onChaptersDeleted() {
 | 
			
		||||
        dismissDeletingDialog()
 | 
			
		||||
        adapter.notifyDataSetChanged()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onChaptersDeletedError(error: Throwable) {
 | 
			
		||||
        dismissDeletingDialog()
 | 
			
		||||
        Timber.e(error, error.message)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun dismissDeletingDialog() {
 | 
			
		||||
        (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onListItemClick(position: Int): Boolean {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,14 +10,16 @@ import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.util.getResourceColor
 | 
			
		||||
import kotlinx.android.synthetic.main.item_chapter.view.*
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import java.text.DateFormat
 | 
			
		||||
import java.text.DecimalFormat
 | 
			
		||||
import java.text.DecimalFormatSymbols
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class ChaptersHolder(private val view: View, private val adapter: ChaptersAdapter, listener: FlexibleViewHolder.OnListItemClickListener) :
 | 
			
		||||
        FlexibleViewHolder(view, adapter, listener) {
 | 
			
		||||
class ChaptersHolder(
 | 
			
		||||
        private val view: View,
 | 
			
		||||
        private val adapter: ChaptersAdapter,
 | 
			
		||||
        listener: FlexibleViewHolder.OnListItemClickListener)
 | 
			
		||||
: FlexibleViewHolder(view, adapter, listener) {
 | 
			
		||||
 | 
			
		||||
    private val readColor = view.context.theme.getResourceColor(android.R.attr.textColorHint)
 | 
			
		||||
    private val unreadColor = view.context.theme.getResourceColor(android.R.attr.textColorPrimary)
 | 
			
		||||
@@ -27,7 +29,10 @@ class ChaptersHolder(private val view: View, private val adapter: ChaptersAdapte
 | 
			
		||||
    private var item: Chapter? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        view.chapter_menu.setOnClickListener { v -> v.post { showPopupMenu(v) } }
 | 
			
		||||
        // We need to post a Runnable to show the popup to make sure that the PopupMenu is
 | 
			
		||||
        // correctly positioned. The reason being that the view may change position before the
 | 
			
		||||
        // PopupMenu is shown.
 | 
			
		||||
        view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onSetValues(chapter: Chapter, manga: Manga?) = with(view) {
 | 
			
		||||
@@ -101,14 +106,14 @@ class ChaptersHolder(private val view: View, private val adapter: ChaptersAdapte
 | 
			
		||||
 | 
			
		||||
        // Set a listener so we are notified if a menu item is clicked
 | 
			
		||||
        popup.setOnMenuItemClickListener { menuItem ->
 | 
			
		||||
            val chapter = Observable.just(item)
 | 
			
		||||
            val chapter = listOf(item)
 | 
			
		||||
 | 
			
		||||
            when (menuItem.itemId) {
 | 
			
		||||
                R.id.action_download -> adapter.fragment.onDownload(chapter)
 | 
			
		||||
                R.id.action_delete -> adapter.fragment.onDelete(chapter)
 | 
			
		||||
                R.id.action_mark_as_read -> adapter.fragment.onMarkAsRead(chapter)
 | 
			
		||||
                R.id.action_mark_as_unread -> adapter.fragment.onMarkAsUnread(chapter)
 | 
			
		||||
                R.id.action_mark_previous_as_read -> adapter.fragment.onMarkPreviousAsRead(item)
 | 
			
		||||
                R.id.action_download -> adapter.fragment.downloadChapters(chapter)
 | 
			
		||||
                R.id.action_delete -> adapter.fragment.deleteChapters(chapter)
 | 
			
		||||
                R.id.action_mark_as_read -> adapter.fragment.markAsRead(chapter)
 | 
			
		||||
                R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(chapter)
 | 
			
		||||
                R.id.action_mark_previous_as_read -> adapter.fragment.markPreviousAsRead(item)
 | 
			
		||||
            }
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,13 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadService
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.base.Source
 | 
			
		||||
import eu.kanade.tachiyomi.event.ChapterCountEvent
 | 
			
		||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
 | 
			
		||||
import eu.kanade.tachiyomi.event.MangaEvent
 | 
			
		||||
import eu.kanade.tachiyomi.event.ReaderEvent
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.util.SharedData
 | 
			
		||||
import rx.Observable
 | 
			
		||||
@@ -163,50 +162,59 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
 | 
			
		||||
        return db.getNextUnreadChapter(manga).executeAsBlocking()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun markChaptersRead(selectedChapters: Observable<Chapter>, read: Boolean) {
 | 
			
		||||
        add(selectedChapters.subscribeOn(Schedulers.io())
 | 
			
		||||
    fun markChaptersRead(selectedChapters: List<Chapter>, read: Boolean) {
 | 
			
		||||
        Observable.from(selectedChapters)
 | 
			
		||||
                .doOnNext { chapter ->
 | 
			
		||||
                    chapter.read = read
 | 
			
		||||
                    if (!read) chapter.last_page_read = 0
 | 
			
		||||
 | 
			
		||||
                    // Delete chapter when marked as read if desired by user.
 | 
			
		||||
                    if (preferences.removeAfterMarkedAsRead() && read) {
 | 
			
		||||
                        deleteChapter(chapter)
 | 
			
		||||
                    if (!read) {
 | 
			
		||||
                        chapter.last_page_read = 0
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .toList()
 | 
			
		||||
                .flatMap { chapters -> db.insertChapters(chapters).asRxObservable() }
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe())
 | 
			
		||||
                .flatMap { db.insertChapters(it).asRxObservable() }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .subscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun markPreviousChaptersAsRead(selected: Chapter) {
 | 
			
		||||
        Observable.from(chapters)
 | 
			
		||||
                .filter { c -> c.chapter_number > -1 && c.chapter_number < selected.chapter_number }
 | 
			
		||||
                .doOnNext { c -> c.read = true }
 | 
			
		||||
                .filter { it.chapter_number > -1 && it.chapter_number < selected.chapter_number }
 | 
			
		||||
                .doOnNext { it.read = true }
 | 
			
		||||
                .toList()
 | 
			
		||||
                .flatMap { chapters -> db.insertChapters(chapters).asRxObservable() }
 | 
			
		||||
                .flatMap { db.insertChapters(it).asRxObservable() }
 | 
			
		||||
                .subscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun downloadChapters(selectedChapters: Observable<Chapter>) {
 | 
			
		||||
        add(selectedChapters.toList()
 | 
			
		||||
    fun downloadChapters(chapters: List<Chapter>) {
 | 
			
		||||
        DownloadService.start(context)
 | 
			
		||||
        downloadManager.downloadChapters(manga, chapters)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun deleteChapters(chapters: List<Chapter>) {
 | 
			
		||||
        val wasRunning = downloadManager.isRunning
 | 
			
		||||
        if (wasRunning) {
 | 
			
		||||
            DownloadService.stop(context)
 | 
			
		||||
        }
 | 
			
		||||
        Observable.from(chapters)
 | 
			
		||||
                .doOnNext { deleteChapter(it) }
 | 
			
		||||
                .toList()
 | 
			
		||||
                .doOnNext { if (onlyDownloaded()) refreshChapters() }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe { downloadManager.onDownloadChaptersEvent(DownloadChaptersEvent(manga, it)) })
 | 
			
		||||
                .subscribeFirst({ view, result ->
 | 
			
		||||
                    view.onChaptersDeleted()
 | 
			
		||||
                    if (wasRunning) {
 | 
			
		||||
                        DownloadService.start(context)
 | 
			
		||||
                    }
 | 
			
		||||
                }, { view, error ->
 | 
			
		||||
                    view.onChaptersDeletedError(error)
 | 
			
		||||
                })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun deleteChapters(selectedChapters: Observable<Chapter>) {
 | 
			
		||||
        add(selectedChapters.subscribe(
 | 
			
		||||
                { chapter -> downloadManager.queue.del(chapter) },
 | 
			
		||||
                { error -> Timber.e(error.message) },
 | 
			
		||||
                {
 | 
			
		||||
                    if (onlyDownloaded())
 | 
			
		||||
                        refreshChapters()
 | 
			
		||||
                }))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun deleteChapter(chapter: Chapter) {
 | 
			
		||||
    private fun deleteChapter(chapter: Chapter) {
 | 
			
		||||
        downloadManager.queue.del(chapter)
 | 
			
		||||
        downloadManager.deleteChapter(source, manga, chapter)
 | 
			
		||||
        chapter.status = Download.NOT_DOWNLOADED
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun revertSortOrder() {
 | 
			
		||||
 
 | 
			
		||||
@@ -336,7 +336,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        db.insertChapter(chapter).asRxObservable().subscribe()
 | 
			
		||||
        db.updateChapterProgress(chapter).asRxObservable().subscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,12 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.recent
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v4.app.DialogFragment
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadService
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.decoration.DividerItemDecoration
 | 
			
		||||
@@ -16,12 +14,11 @@ import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.main.MainActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.getResourceDrawable
 | 
			
		||||
import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
 | 
			
		||||
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_recent_chapters.*
 | 
			
		||||
import nucleus.factory.RequiresPresenter
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fragment that shows recent chapters.
 | 
			
		||||
@@ -143,78 +140,57 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Start downloading chapter
 | 
			
		||||
 | 
			
		||||
     * @param chapters selected chapters
 | 
			
		||||
     * @param manga    manga that belongs to chapter
 | 
			
		||||
     * @return true
 | 
			
		||||
     * Mark chapter as read
 | 
			
		||||
     *
 | 
			
		||||
     * @param item selected chapter with manga
 | 
			
		||||
     */
 | 
			
		||||
    fun onDownload(chapters: Observable<Chapter>, manga: Manga): Boolean {
 | 
			
		||||
        // Start the download service.
 | 
			
		||||
        DownloadService.start(activity)
 | 
			
		||||
    fun markAsRead(item: MangaChapter) {
 | 
			
		||||
        presenter.markChapterRead(item.chapter, true)
 | 
			
		||||
        if (presenter.preferences.removeAfterMarkedAsRead()) {
 | 
			
		||||
            deleteChapter(item)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        // Refresh data on download competition.
 | 
			
		||||
        val observable = chapters
 | 
			
		||||
                .doOnCompleted({
 | 
			
		||||
                    adapter.notifyDataSetChanged()
 | 
			
		||||
                    presenter.start(presenter.CHAPTER_STATUS_CHANGES)
 | 
			
		||||
                })
 | 
			
		||||
    /**
 | 
			
		||||
     * Mark chapter as unread
 | 
			
		||||
     *
 | 
			
		||||
     * @param item selected chapter with manga
 | 
			
		||||
     */
 | 
			
		||||
    fun markAsUnread(item: MangaChapter) {
 | 
			
		||||
        presenter.markChapterRead(item.chapter, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        // Download chapter.
 | 
			
		||||
        presenter.downloadChapter(observable, manga)
 | 
			
		||||
        return true
 | 
			
		||||
    /**
 | 
			
		||||
     * Start downloading chapter
 | 
			
		||||
     *
 | 
			
		||||
     * @param item selected chapter with manga
 | 
			
		||||
     */
 | 
			
		||||
    fun downloadChapter(item: MangaChapter) {
 | 
			
		||||
        presenter.downloadChapter(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Start deleting chapter
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapters selected chapters
 | 
			
		||||
     * @param manga manga that belongs to chapter
 | 
			
		||||
     * @return success of deletion.
 | 
			
		||||
     * @param item selected chapter with manga
 | 
			
		||||
     */
 | 
			
		||||
    fun onDelete(chapters: Observable<Chapter>, manga: Manga): Boolean {
 | 
			
		||||
        //Create observable
 | 
			
		||||
        val observable = chapters
 | 
			
		||||
                .concatMap { chapter ->
 | 
			
		||||
                    presenter.deleteChapter(chapter, manga)
 | 
			
		||||
                    Observable.just(chapter)
 | 
			
		||||
                }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .doOnNext { chapter ->
 | 
			
		||||
                    chapter.status = Download.NOT_DOWNLOADED
 | 
			
		||||
                }
 | 
			
		||||
                .doOnCompleted { adapter.notifyDataSetChanged() }
 | 
			
		||||
 | 
			
		||||
        // Delete chapters with observable
 | 
			
		||||
        presenter.deleteChapters(observable)
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
    fun deleteChapter(item: MangaChapter) {
 | 
			
		||||
        DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
 | 
			
		||||
        presenter.deleteChapter(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mark chapter as read
 | 
			
		||||
 | 
			
		||||
     * @param chapters selected chapter
 | 
			
		||||
     * @return true
 | 
			
		||||
     */
 | 
			
		||||
    fun onMarkAsRead(chapters: Observable<Chapter>, manga : Manga): Boolean {
 | 
			
		||||
        // Set marked as read
 | 
			
		||||
        presenter.markChaptersRead(chapters, manga, true)
 | 
			
		||||
        return true
 | 
			
		||||
    fun onChaptersDeleted() {
 | 
			
		||||
        dismissDeletingDialog()
 | 
			
		||||
        adapter.notifyDataSetChanged()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mark chapter as unread
 | 
			
		||||
 | 
			
		||||
     * @param chapters selected chapter
 | 
			
		||||
     * @return true
 | 
			
		||||
     */
 | 
			
		||||
    fun onMarkAsUnread(chapters: Observable<Chapter> , manga : Manga): Boolean {
 | 
			
		||||
        // Set marked as unread
 | 
			
		||||
        presenter.markChaptersRead(chapters, manga, false)
 | 
			
		||||
        return true
 | 
			
		||||
    fun onChaptersDeletedError(error: Throwable) {
 | 
			
		||||
        dismissDeletingDialog()
 | 
			
		||||
        Timber.e(error, error.message)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun dismissDeletingDialog() {
 | 
			
		||||
        (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +1,13 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.recent
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.PopupMenu
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.util.getResourceColor
 | 
			
		||||
import kotlinx.android.synthetic.main.item_recent_chapter.view.*
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Holder that contains chapter item
 | 
			
		||||
@@ -32,7 +29,7 @@ class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapte
 | 
			
		||||
    /**
 | 
			
		||||
     * Color of unread chapter
 | 
			
		||||
     */
 | 
			
		||||
    private var unreadColor  = view.context.theme.getResourceColor(android.R.attr.textColorPrimary)
 | 
			
		||||
    private var unreadColor = view.context.theme.getResourceColor(android.R.attr.textColorPrimary)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Object containing chapter information
 | 
			
		||||
@@ -40,9 +37,10 @@ class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapte
 | 
			
		||||
    private var mangaChapter: MangaChapter? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        //Set OnClickListener for download menu
 | 
			
		||||
        itemView.chapterMenu.setOnClickListener { v -> v.post({ showPopupMenu(v) }) }
 | 
			
		||||
 | 
			
		||||
        // We need to post a Runnable to show the popup to make sure that the PopupMenu is
 | 
			
		||||
        // correctly positioned. The reason being that the view may change position before the
 | 
			
		||||
        // PopupMenu is shown.
 | 
			
		||||
        itemView.chapterMenu.setOnClickListener { it.post({ showPopupMenu(it) }) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -120,15 +118,14 @@ class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapte
 | 
			
		||||
 | 
			
		||||
            // Set a listener so we are notified if a menu item is clicked
 | 
			
		||||
            popup.setOnMenuItemClickListener { menuItem ->
 | 
			
		||||
                val chapterObservable = Observable.just<Chapter>(it.chapter)
 | 
			
		||||
 | 
			
		||||
                when (menuItem.itemId) {
 | 
			
		||||
                    R.id.action_download -> adapter.fragment.onDownload(chapterObservable, it.manga)
 | 
			
		||||
                    R.id.action_delete -> adapter.fragment.onDelete(chapterObservable, it.manga)
 | 
			
		||||
                    R.id.action_mark_as_read -> adapter.fragment.onMarkAsRead(chapterObservable, it.manga);
 | 
			
		||||
                    R.id.action_mark_as_unread -> adapter.fragment.onMarkAsUnread(chapterObservable, it.manga);
 | 
			
		||||
                    R.id.action_download -> adapter.fragment.downloadChapter(it)
 | 
			
		||||
                    R.id.action_delete -> adapter.fragment.deleteChapter(it)
 | 
			
		||||
                    R.id.action_mark_as_read -> adapter.fragment.markAsRead(it)
 | 
			
		||||
                    R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(it)
 | 
			
		||||
                }
 | 
			
		||||
                false
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,10 +6,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadService
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
@@ -250,59 +250,69 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download selected chapter
 | 
			
		||||
     * @param selectedChapter chapter that is selected
 | 
			
		||||
     * *
 | 
			
		||||
     * @param manga manga that belongs to chapter
 | 
			
		||||
     * Mark selected chapter as read
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapter selected chapter
 | 
			
		||||
     * @param read read status
 | 
			
		||||
     */
 | 
			
		||||
    fun downloadChapter(selectedChapter: Observable<Chapter>, manga: Manga) {
 | 
			
		||||
        add(selectedChapter.toList()
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe { downloadManager.onDownloadChaptersEvent(DownloadChaptersEvent(manga, it)) })
 | 
			
		||||
    fun markChapterRead(chapter: Chapter, read: Boolean) {
 | 
			
		||||
        Observable.just(chapter)
 | 
			
		||||
                .doOnNext { chapter ->
 | 
			
		||||
                    chapter.read = read
 | 
			
		||||
                    if (!read) {
 | 
			
		||||
                        chapter.last_page_read = 0
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .flatMap { db.updateChapterProgress(it).asRxObservable() }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .subscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download selected chapter
 | 
			
		||||
     *
 | 
			
		||||
     * @param item chapter that is selected
 | 
			
		||||
     */
 | 
			
		||||
    fun downloadChapter(item: MangaChapter) {
 | 
			
		||||
        DownloadService.start(context)
 | 
			
		||||
        downloadManager.downloadChapters(item.manga, listOf(item.chapter))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete selected chapter
 | 
			
		||||
     *
 | 
			
		||||
     * @param item chapter that are selected
 | 
			
		||||
     */
 | 
			
		||||
    fun deleteChapter(item: MangaChapter) {
 | 
			
		||||
        val wasRunning = downloadManager.isRunning
 | 
			
		||||
        if (wasRunning) {
 | 
			
		||||
            DownloadService.stop(context)
 | 
			
		||||
        }
 | 
			
		||||
        Observable.just(item)
 | 
			
		||||
                .doOnNext { deleteChapter(it.chapter, it.manga) }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeFirst({ view, result ->
 | 
			
		||||
                    view.onChaptersDeleted()
 | 
			
		||||
                    if (wasRunning) {
 | 
			
		||||
                        DownloadService.start(context)
 | 
			
		||||
                    }
 | 
			
		||||
                }, { view, error ->
 | 
			
		||||
                    view.onChaptersDeletedError(error)
 | 
			
		||||
                })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete selected chapter
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapter chapter that is selected
 | 
			
		||||
     * *
 | 
			
		||||
     * @param manga manga that belongs to chapter
 | 
			
		||||
     */
 | 
			
		||||
    fun deleteChapter(chapter: Chapter, manga: Manga) {
 | 
			
		||||
        val source = sourceManager.get(manga.source)!!
 | 
			
		||||
    private fun deleteChapter(chapter: Chapter, manga: Manga) {
 | 
			
		||||
        val source = sourceManager.get(manga.source) ?: return
 | 
			
		||||
        downloadManager.queue.del(chapter)
 | 
			
		||||
        downloadManager.deleteChapter(source, manga, chapter)
 | 
			
		||||
        chapter.status = Download.NOT_DOWNLOADED
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete selected chapter observable
 | 
			
		||||
     * @param selectedChapters chapter that are selected
 | 
			
		||||
     */
 | 
			
		||||
    fun deleteChapters(selectedChapters: Observable<Chapter>) {
 | 
			
		||||
        add(selectedChapters
 | 
			
		||||
                .subscribe(
 | 
			
		||||
                        { chapter -> downloadManager.queue.del(chapter) })
 | 
			
		||||
                        { error -> Timber.e(error.message) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mark selected chapter as read
 | 
			
		||||
     * @param selectedChapters chapter that is selected
 | 
			
		||||
     * *
 | 
			
		||||
     * @param read read status
 | 
			
		||||
     */
 | 
			
		||||
    fun markChaptersRead(selectedChapters: Observable<Chapter>, manga: Manga, read: Boolean) {
 | 
			
		||||
        add(selectedChapters.subscribeOn(Schedulers.io())
 | 
			
		||||
                .doOnNext { chapter ->
 | 
			
		||||
                    chapter.read = read
 | 
			
		||||
                    if (!read) chapter.last_page_read = 0
 | 
			
		||||
 | 
			
		||||
                    // Delete chapter when marked as read if desired by user.
 | 
			
		||||
                    if (preferences.removeAfterMarkedAsRead() && read) {
 | 
			
		||||
                        deleteChapter(chapter,manga)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .toList()
 | 
			
		||||
                .flatMap { chapters -> db.insertChapters(chapters).asRxObservable() }
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package eu.kanade.tachiyomi.widget
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v4.app.DialogFragment
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
class DeletingChaptersDialog : DialogFragment() {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val TAG = "deleting_dialog"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateDialog(savedState: Bundle?): Dialog {
 | 
			
		||||
        return MaterialDialog.Builder(activity)
 | 
			
		||||
                .progress(true, 0)
 | 
			
		||||
                .content(R.string.deleting)
 | 
			
		||||
                .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user