diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index 18f922d93..39245162d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/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) = db.put().objects(mangas).prepare() - - fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() - - fun deleteMangas(mangas: List) = 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 { - // 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 { - // 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) = db.put().objects(chapters).prepare() - - // Add new chapters or delete if the source deletes them - open fun insertOrRemoveChapters(manga: Manga, sourceChapters: List, source: Source): Observable> { - 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() - 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) = 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) = 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) = db.put().objects(categories).prepare() - - fun deleteCategory(category: Category) = db.delete().`object`(category).prepare() - - fun deleteCategories(categories: List) = db.delete().objects(categories).prepare() - - fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare() - - fun insertMangasCategories(mangasCategories: List) = db.put().objects(mangasCategories).prepare() - - fun deleteOldMangasCategories(mangas: List) = 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, mangas: List) { - inTransaction { - deleteOldMangasCategories(mangas).executeAsBlocking() - insertMangasCategories(mangasCategories).executeAsBlocking() - } - } + inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbExtensions.kt new file mode 100644 index 000000000..9c60968a8 --- /dev/null +++ b/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 StorIOSQLite.inTransactionReturn(block: () -> T): T { + internal().beginTransaction() + try { + val result = block() + internal().setTransactionSuccessful() + return result + } finally { + internal().endTransaction() + } +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbProvider.kt new file mode 100644 index 000000000..7af8dff0c --- /dev/null +++ b/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 + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt new file mode 100644 index 000000000..b873bc631 --- /dev/null +++ b/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) = db.put().objects(categories).prepare() + + fun deleteCategory(category: Category) = db.delete().`object`(category).prepare() + + fun deleteCategories(categories: List) = db.delete().objects(categories).prepare() + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt new file mode 100644 index 000000000..87f73959d --- /dev/null +++ b/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 { + // 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 { + // 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) = 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, source: Source): Observable> { + 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() + 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) = db.delete().objects(chapters).prepare() + + fun updateChapterProgress(chapter: Chapter) = db.put() + .`object`(chapter) + .withPutResolver(ChapterProgressPutResolver.instance) + .prepare() + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaCategoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaCategoryQueries.kt new file mode 100644 index 000000000..dfe335268 --- /dev/null +++ b/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) = db.put().objects(mangasCategories).prepare() + + fun deleteOldMangasCategories(mangas: List) = 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, mangas: List) { + db.inTransaction { + deleteOldMangasCategories(mangas).executeAsBlocking() + insertMangasCategories(mangasCategories).executeAsBlocking() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt new file mode 100644 index 000000000..dc6866e26 --- /dev/null +++ b/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) = db.put().objects(mangas).prepare() + + fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() + + fun deleteMangas(mangas: List) = db.delete().objects(mangas).prepare() + + fun deleteMangasNotInLibrary() = db.delete() + .byQuery(DeleteQuery.builder() + .table(MangaTable.TABLE) + .where("${MangaTable.COLUMN_FAVORITE} = ?") + .whereArgs(0) + .build()) + .prepare() + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt new file mode 100644 index 000000000..47995ab82 --- /dev/null +++ b/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) = 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() + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt similarity index 81% rename from app/src/main/java/eu/kanade/tachiyomi/data/database/RawQueries.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index a72dce1ab..99cfffae4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/RawQueries.kt +++ b/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}" \ No newline at end of file + "WHERE ${MangaCategory.COLUMN_MANGA_ID} = ?" \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterProgressPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterProgressPutResolver.kt new file mode 100644 index 000000000..f8f160498 --- /dev/null +++ b/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() { + + 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) + } + +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 41a930c88..815cc060c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/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) { val source = sourceManager.get(manga.source) // Used to avoid downloading chapters with the same name val addedChapters = ArrayList() val pending = ArrayList() - for (chapter in event.chapters) { + for (chapter in chapters) { if (addedChapters.contains(chapter.name)) continue diff --git a/app/src/main/java/eu/kanade/tachiyomi/event/DownloadChaptersEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/event/DownloadChaptersEvent.kt deleted file mode 100644 index 8e7483c4e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/event/DownloadChaptersEvent.kt +++ /dev/null @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt index d14e51283..8d866268d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt +++ b/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(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener { @@ -40,6 +39,7 @@ class ChaptersFragment : BaseRxFragment(), ActionMode.Callbac fun newInstance(): ChaptersFragment { return ChaptersFragment() } + } /** @@ -73,7 +73,7 @@ class ChaptersFragment : BaseRxFragment(), 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(), ActionMode.Callbac chapters = chapters.subList(0, 10) } } - onDownload(Observable.from(chapters)) + downloadChapters(chapters) } } .show() @@ -278,11 +278,11 @@ class ChaptersFragment : BaseRxFragment(), 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(), ActionMode.Callbac actionMode = null } - fun getSelectedChapters(): Observable { - val chapters = adapter.selectedItems.map { adapter.getItem(it) } - return Observable.from(chapters) + fun getSelectedChapters(): List { + 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) { + fun markAsRead(chapters: List) { presenter.markChaptersRead(chapters, true) + if (presenter.preferences.removeAfterMarkedAsRead()) { + deleteChapters(chapters) + } } - fun onMarkAsUnread(chapters: Observable) { + fun markAsUnread(chapters: List) { presenter.markChaptersRead(chapters, false) } - fun onMarkPreviousAsRead(chapter: Chapter) { + fun markPreviousAsRead(chapter: Chapter) { presenter.markPreviousChaptersAsRead(chapter) } - fun onDownload(chapters: Observable) { - DownloadService.start(activity) - - val observable = chapters.doOnCompleted { adapter.notifyDataSetChanged() } - - presenter.downloadChapters(observable) + fun downloadChapters(chapters: List) { destroyActionModeIfNeeded() + presenter.downloadChapters(chapters) } - fun onDelete(chapters: Observable) { - val size = adapter.selectedItemCount - - val dialog = MaterialDialog.Builder(activity) - .title(R.string.deleting) - .progress(false, size, true) - .cancelable(false) - .show() - - val observable = chapters - .concatMap { chapter -> - presenter.deleteChapter(chapter) - Observable.just(chapter) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { chapter -> - dialog.incrementProgress(1) - chapter.status = Download.NOT_DOWNLOADED - } - .doOnCompleted { adapter.notifyDataSetChanged() } - .doAfterTerminate { dialog.dismiss() } - - presenter.deleteChapters(observable) + fun deleteChapters(chapters: List) { destroyActionModeIfNeeded() + DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) + presenter.deleteChapters(chapters) + } + + fun onChaptersDeleted() { + dismissDeletingDialog() + adapter.notifyDataSetChanged() + } + + fun onChaptersDeletedError(error: Throwable) { + dismissDeletingDialog() + Timber.e(error, error.message) + } + + fun dismissDeletingDialog() { + (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss() } override fun onListItemClick(position: Int): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.kt index 5e35c3e39..389ce33e2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.kt +++ b/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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index ea83b97c6..b00c7028a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/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() { return db.getNextUnreadChapter(manga).executeAsBlocking() } - fun markChaptersRead(selectedChapters: Observable, read: Boolean) { - add(selectedChapters.subscribeOn(Schedulers.io()) + fun markChaptersRead(selectedChapters: List, 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) { - add(selectedChapters.toList() + fun downloadChapters(chapters: List) { + DownloadService.start(context) + downloadManager.downloadChapters(manga, chapters) + } + + fun deleteChapters(chapters: List) { + val wasRunning = downloadManager.isRunning + if (wasRunning) { + DownloadService.stop(context) + } + Observable.from(chapters) + .doOnNext { deleteChapter(it) } + .toList() + .doOnNext { if (onlyDownloaded()) refreshChapters() } + .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe { downloadManager.onDownloadChaptersEvent(DownloadChaptersEvent(manga, it)) }) + .subscribeFirst({ view, result -> + view.onChaptersDeleted() + if (wasRunning) { + DownloadService.start(context) + } + }, { view, error -> + view.onChaptersDeletedError(error) + }) } - fun deleteChapters(selectedChapters: Observable) { - add(selectedChapters.subscribe( - { chapter -> downloadManager.queue.del(chapter) }, - { error -> Timber.e(error.message) }, - { - if (onlyDownloaded()) - refreshChapters() - })) - } - - fun deleteChapter(chapter: Chapter) { + private fun deleteChapter(chapter: Chapter) { + downloadManager.queue.del(chapter) downloadManager.deleteChapter(source, manga, chapter) + chapter.status = Download.NOT_DOWNLOADED } fun revertSortOrder() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 748afa58d..e233d8234 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -336,7 +336,7 @@ class ReaderPresenter : BasePresenter() { } } } - db.insertChapter(chapter).asRxObservable().subscribe() + db.updateChapterProgress(chapter).asRxObservable().subscribe() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersFragment.kt index a84b86f53..2b114236c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersFragment.kt +++ b/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(), 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, manga: Manga): Boolean { - // Start the download service. - DownloadService.start(activity) + fun markAsRead(item: MangaChapter) { + presenter.markChapterRead(item.chapter, true) + if (presenter.preferences.removeAfterMarkedAsRead()) { + deleteChapter(item) + } + } - // Refresh data on download competition. - val observable = chapters - .doOnCompleted({ - adapter.notifyDataSetChanged() - presenter.start(presenter.CHAPTER_STATUS_CHANGES) - }) + /** + * Mark chapter as unread + * + * @param item selected chapter with manga + */ + fun markAsUnread(item: MangaChapter) { + presenter.markChapterRead(item.chapter, false) + } - // Download chapter. - presenter.downloadChapter(observable, manga) - return true + /** + * Start downloading chapter + * + * @param item selected chapter with manga + */ + fun downloadChapter(item: MangaChapter) { + presenter.downloadChapter(item) } /** * Start deleting chapter * - * @param chapters selected chapters - * @param manga manga that belongs to chapter - * @return success of deletion. + * @param item selected chapter with manga */ - fun onDelete(chapters: Observable, manga: Manga): Boolean { - //Create observable - val observable = chapters - .concatMap { chapter -> - presenter.deleteChapter(chapter, manga) - Observable.just(chapter) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { chapter -> - chapter.status = Download.NOT_DOWNLOADED - } - .doOnCompleted { adapter.notifyDataSetChanged() } - - // Delete chapters with observable - presenter.deleteChapters(observable) - - return true + fun deleteChapter(item: MangaChapter) { + DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) + presenter.deleteChapter(item) } - /** - * Mark chapter as read - - * @param chapters selected chapter - * @return true - */ - fun onMarkAsRead(chapters: Observable, manga : Manga): Boolean { - // Set marked as read - presenter.markChaptersRead(chapters, manga, true) - return true + fun onChaptersDeleted() { + dismissDeletingDialog() + adapter.notifyDataSetChanged() } - /** - * Mark chapter as unread - - * @param chapters selected chapter - * @return true - */ - fun onMarkAsUnread(chapters: Observable , manga : Manga): Boolean { - // Set marked as unread - presenter.markChaptersRead(chapters, manga, false) - return true + fun onChaptersDeletedError(error: Throwable) { + dismissDeletingDialog() + Timber.e(error, error.message) } + fun dismissDeletingDialog() { + (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss() + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.kt index 601e80b59..e1e4d20cb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.kt +++ b/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(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 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.kt index 97e5ee33d..b482048e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.kt +++ b/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() { } /** - * 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, manga: Manga) { - add(selectedChapter.toList() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { downloadManager.onDownloadChaptersEvent(DownloadChaptersEvent(manga, it)) }) + fun markChapterRead(chapter: Chapter, read: Boolean) { + Observable.just(chapter) + .doOnNext { chapter -> + chapter.read = read + if (!read) { + chapter.last_page_read = 0 + } + } + .flatMap { db.updateChapterProgress(it).asRxObservable() } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Download selected chapter + * + * @param item chapter that is selected + */ + fun downloadChapter(item: MangaChapter) { + DownloadService.start(context) + downloadManager.downloadChapters(item.manga, listOf(item.chapter)) } /** * Delete selected chapter + * + * @param item chapter that are selected + */ + fun deleteChapter(item: MangaChapter) { + val wasRunning = downloadManager.isRunning + if (wasRunning) { + DownloadService.stop(context) + } + Observable.just(item) + .doOnNext { deleteChapter(it.chapter, it.manga) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, result -> + view.onChaptersDeleted() + if (wasRunning) { + DownloadService.start(context) + } + }, { view, error -> + view.onChaptersDeletedError(error) + }) + } + + /** + * Delete selected chapter + * * @param chapter chapter that is selected - * * * @param manga manga that belongs to chapter */ - fun deleteChapter(chapter: Chapter, manga: Manga) { - val source = sourceManager.get(manga.source)!! + private fun deleteChapter(chapter: Chapter, manga: Manga) { + val source = sourceManager.get(manga.source) ?: return + downloadManager.queue.del(chapter) downloadManager.deleteChapter(source, manga, chapter) + chapter.status = Download.NOT_DOWNLOADED } - /** - * Delete selected chapter observable - * @param selectedChapters chapter that are selected - */ - fun deleteChapters(selectedChapters: Observable) { - add(selectedChapters - .subscribe( - { chapter -> downloadManager.queue.del(chapter) }) - { error -> Timber.e(error.message) }) - } - - /** - * Mark selected chapter as read - * @param selectedChapters chapter that is selected - * * - * @param read read status - */ - fun markChaptersRead(selectedChapters: Observable, 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()) - } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DeletingChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DeletingChaptersDialog.kt new file mode 100644 index 000000000..4a1d86161 --- /dev/null +++ b/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() + } + +} \ No newline at end of file