Browse Source

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

len 10 năm trước cách đây
mục cha
commit
5e24054a0b
20 tập tin đã thay đổi với 650 bổ sung525 xóa
  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()
+    }
+
+}