ソースを参照

Database queries are now separated by table. Improve how the app creates downloads

len 9 年 前
コミット
5e24054a0b
20 ファイル変更650 行追加525 行削除
  1. 8 298
      app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt
  2. 25 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/DbExtensions.kt
  3. 9 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/DbProvider.kt
  4. 36 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt
  5. 158 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt
  6. 32 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaCategoryQueries.kt
  7. 75 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt
  8. 46 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt
  9. 6 12
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt
  10. 38 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterProgressPutResolver.kt
  11. 5 6
      app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt
  12. 0 6
      app/src/main/java/eu/kanade/tachiyomi/event/DownloadChaptersEvent.kt
  13. 37 46
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt
  14. 15 10
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.kt
  15. 36 28
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt
  16. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
  17. 39 63
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersFragment.kt
  18. 10 13
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.kt
  19. 52 42
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.kt
  20. 22 0
      app/src/main/java/eu/kanade/tachiyomi/widget/DeletingChaptersDialog.kt

+ 8 - 298
app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt

@@ -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)
 
 }

+ 25 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/DbExtensions.kt

@@ -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()
+    }
+}
+

+ 9 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/DbProvider.kt

@@ -0,0 +1,9 @@
+package eu.kanade.tachiyomi.data.database
+
+import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
+
+interface DbProvider {
+
+    val db: DefaultStorIOSQLite
+
+}

+ 36 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt

@@ -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()
+
+}

+ 158 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt

@@ -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()
+
+}

+ 32 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaCategoryQueries.kt

@@ -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()
+        }
+    }
+
+}

+ 75 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt

@@ -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()
+
+}

+ 46 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt

@@ -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()
+
+}

+ 6 - 12
app/src/main/java/eu/kanade/tachiyomi/data/database/RawQueries.kt → app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt

@@ -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} = ?"

+ 38 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterProgressPutResolver.kt

@@ -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)
+    }
+
+}
+

+ 5 - 6
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt

@@ -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
 

+ 0 - 6
app/src/main/java/eu/kanade/tachiyomi/event/DownloadChaptersEvent.kt

@@ -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>)

+ 37 - 46
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt

@@ -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
+    fun deleteChapters(chapters: List<Chapter>) {
+        destroyActionModeIfNeeded()
+        DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
+        presenter.deleteChapters(chapters)
+    }
 
-        val dialog = MaterialDialog.Builder(activity)
-                .title(R.string.deleting)
-                .progress(false, size, true)
-                .cancelable(false)
-                .show()
+    fun onChaptersDeleted() {
+        dismissDeletingDialog()
+        adapter.notifyDataSetChanged()
+    }
 
-        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() }
+    fun onChaptersDeletedError(error: Throwable) {
+        dismissDeletingDialog()
+        Timber.e(error, error.message)
+    }
 
-        presenter.deleteChapters(observable)
-        destroyActionModeIfNeeded()
+    fun dismissDeletingDialog() {
+        (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
     }
 
     override fun onListItemClick(position: Int): Boolean {

+ 15 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.kt

@@ -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
         }

+ 36 - 28
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt

@@ -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()
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe { downloadManager.onDownloadChaptersEvent(DownloadChaptersEvent(manga, it)) })
+    fun downloadChapters(chapters: List<Chapter>) {
+        DownloadService.start(context)
+        downloadManager.downloadChapters(manga, chapters)
     }
 
-    fun deleteChapters(selectedChapters: Observable<Chapter>) {
-        add(selectedChapters.subscribe(
-                { chapter -> downloadManager.queue.del(chapter) },
-                { error -> Timber.e(error.message) },
-                {
-                    if (onlyDownloaded())
-                        refreshChapters()
-                }))
+    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())
+                .subscribeFirst({ view, result ->
+                    view.onChaptersDeleted()
+                    if (wasRunning) {
+                        DownloadService.start(context)
+                    }
+                }, { view, error ->
+                    view.onChaptersDeletedError(error)
+                })
     }
 
-    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() {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -336,7 +336,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
                 }
             }
         }
-        db.insertChapter(chapter).asRxObservable().subscribe()
+        db.updateChapterProgress(chapter).asRxObservable().subscribe()
     }
 
     /**

+ 39 - 63
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersFragment.kt

@@ -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)
-
-        // Refresh data on download competition.
-        val observable = chapters
-                .doOnCompleted({
-                    adapter.notifyDataSetChanged()
-                    presenter.start(presenter.CHAPTER_STATUS_CHANGES)
-                })
-
-        // Download chapter.
-        presenter.downloadChapter(observable, manga)
-        return true
+    fun markAsRead(item: MangaChapter) {
+        presenter.markChapterRead(item.chapter, true)
+        if (presenter.preferences.removeAfterMarkedAsRead()) {
+            deleteChapter(item)
+        }
     }
 
     /**
-     * Start deleting chapter
+     * Mark chapter as unread
      *
-     * @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 markAsUnread(item: MangaChapter) {
+        presenter.markChapterRead(item.chapter, false)
     }
 
     /**
-     * Mark chapter as read
-
-     * @param chapters selected chapter
-     * @return true
+     * Start downloading chapter
+     *
+     * @param item selected chapter with manga
      */
-    fun onMarkAsRead(chapters: Observable<Chapter>, manga : Manga): Boolean {
-        // Set marked as read
-        presenter.markChaptersRead(chapters, manga, true)
-        return true
+    fun downloadChapter(item: MangaChapter) {
+        presenter.downloadChapter(item)
     }
 
     /**
-     * Mark chapter as unread
-
-     * @param chapters selected chapter
-     * @return true
+     * Start deleting chapter
+     *
+     * @param item selected chapter with manga
      */
-    fun onMarkAsUnread(chapters: Observable<Chapter> , manga : Manga): Boolean {
-        // Set marked as unread
-        presenter.markChaptersRead(chapters, manga, false)
-        return true
+    fun deleteChapter(item: MangaChapter) {
+        DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
+        presenter.deleteChapter(item)
     }
 
+    fun onChaptersDeleted() {
+        dismissDeletingDialog()
+        adapter.notifyDataSetChanged()
+    }
+
+    fun onChaptersDeletedError(error: Throwable) {
+        dismissDeletingDialog()
+        Timber.e(error, error.message)
+    }
+
+    fun dismissDeletingDialog() {
+        (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
+    }
 
 }

+ 10 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.kt

@@ -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
             }
 
         }

+ 52 - 42
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.kt

@@ -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()
     }
 
     /**
-     * Delete selected chapter
-     * @param chapter chapter that is selected
-     * *
-     * @param manga manga that belongs to chapter
+     * Download selected chapter
+     *
+     * @param item chapter that is selected
      */
-    fun deleteChapter(chapter: Chapter, manga: Manga) {
-        val source = sourceManager.get(manga.source)!!
-        downloadManager.deleteChapter(source, manga, chapter)
+    fun downloadChapter(item: MangaChapter) {
+        DownloadService.start(context)
+        downloadManager.downloadChapters(item.manga, listOf(item.chapter))
     }
 
     /**
-     * Delete selected chapter observable
-     * @param selectedChapters chapter that are selected
+     * Delete selected chapter
+     *
+     * @param item chapter that are selected
      */
-    fun deleteChapters(selectedChapters: Observable<Chapter>) {
-        add(selectedChapters
-                .subscribe(
-                        { chapter -> downloadManager.queue.del(chapter) })
-                        { error -> Timber.e(error.message) })
+    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)
+                })
     }
 
     /**
-     * Mark selected chapter as read
-     * @param selectedChapters chapter that is selected
-     * *
-     * @param read read status
+     * Delete selected chapter
+     *
+     * @param chapter chapter that is selected
+     * @param manga manga that belongs to chapter
      */
-    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())
+    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
     }
+
 }

+ 22 - 0
app/src/main/java/eu/kanade/tachiyomi/widget/DeletingChaptersDialog.kt

@@ -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()
+    }
+
+}