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

This commit is contained in:
len 2016-05-02 22:33:36 +02:00
parent af2b886599
commit 5e24054a0b
20 changed files with 657 additions and 532 deletions

View File

@ -1,26 +1,17 @@
package eu.kanade.tachiyomi.data.database package eu.kanade.tachiyomi.data.database
import android.content.Context 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.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.models.*
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver import eu.kanade.tachiyomi.data.database.queries.*
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.*
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)) .sqliteOpenHelper(DbOpenHelper(context))
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping()) .addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping()) .addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
@ -29,287 +20,6 @@ open class DatabaseHelper(context: Context) {
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping()) .addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
.build() .build()
inline fun inTransaction(func: DatabaseHelper.() -> Unit) { inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
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()
}
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory 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. * 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} " + "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} " + "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" "ORDER BY ${Chapter.COLUMN_DATE_UPLOAD} DESC"
/** /**
* Query to get the categorias for a manga. * Query to get the categories for a manga.
*
* @param manga the manga.
*/ */
fun getCategoriesForMangaQuery(manga: MangaModel) = fun getCategoriesForMangaQuery() =
"SELECT ${Category.TABLE}.* FROM ${Category.TABLE} " + "SELECT ${Category.TABLE}.* FROM ${Category.TABLE} " +
"JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COLUMN_ID} = " + "JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COLUMN_ID} = " +
"${MangaCategory.TABLE}.${MangaCategory.COLUMN_CATEGORY_ID} " + "${MangaCategory.TABLE}.${MangaCategory.COLUMN_CATEGORY_ID} " +
"WHERE ${MangaCategory.COLUMN_MANGA_ID} = ${manga.id}" "WHERE ${MangaCategory.COLUMN_MANGA_ID} = ?"

View File

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

View File

@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.base.Source import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.util.*
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
@ -45,7 +44,8 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val PAGE_LIST_FILE = "index.json" val PAGE_LIST_FILE = "index.json"
@Volatile private var isRunning: Boolean = false @Volatile var isRunning: Boolean = false
private set
private fun initializeSubscriptions() { private fun initializeSubscriptions() {
downloadsSubscription?.unsubscribe() 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 // Create a download object for every chapter and add them to the downloads queue
fun onDownloadChaptersEvent(event: DownloadChaptersEvent) { fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
val manga = event.manga
val source = sourceManager.get(manga.source) val source = sourceManager.get(manga.source)
// Used to avoid downloading chapters with the same name // Used to avoid downloading chapters with the same name
val addedChapters = ArrayList<String>() val addedChapters = ArrayList<String>()
val pending = ArrayList<Download>() val pending = ArrayList<Download>()
for (chapter in event.chapters) { for (chapter in chapters) {
if (addedChapters.contains(chapter.name)) if (addedChapters.contains(chapter.name))
continue continue

View File

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

View File

@ -4,6 +4,7 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import android.view.* import android.view.*
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@ -11,7 +12,6 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga 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.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.decoration.DividerItemDecoration 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.getCoordinates
import eu.kanade.tachiyomi.util.getResourceDrawable import eu.kanade.tachiyomi.util.getResourceDrawable
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
import kotlinx.android.synthetic.main.fragment_manga_chapters.* import kotlinx.android.synthetic.main.fragment_manga_chapters.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
import rx.Observable import timber.log.Timber
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
@RequiresPresenter(ChaptersPresenter::class) @RequiresPresenter(ChaptersPresenter::class)
class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener { class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
@ -40,6 +39,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
fun newInstance(): ChaptersFragment { fun newInstance(): ChaptersFragment {
return ChaptersFragment() return ChaptersFragment()
} }
} }
/** /**
@ -73,7 +73,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
swipe_refresh.setOnRefreshListener { fetchChapters() } swipe_refresh.setOnRefreshListener { fetchChapters() }
fab.setOnClickListener { v -> fab.setOnClickListener {
val chapter = presenter.getNextUnreadChapter() val chapter = presenter.getNextUnreadChapter()
if (chapter != null) { if (chapter != null) {
// Create animation listener // Create animation listener
@ -252,7 +252,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
chapters = chapters.subList(0, 10) chapters = chapters.subList(0, 10)
} }
} }
onDownload(Observable.from(chapters)) downloadChapters(chapters)
} }
} }
.show() .show()
@ -278,11 +278,11 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_select_all -> onSelectAll() R.id.action_select_all -> selectAll()
R.id.action_mark_as_read -> onMarkAsRead(getSelectedChapters()) R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
R.id.action_mark_as_unread -> onMarkAsUnread(getSelectedChapters()) R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
R.id.action_download -> onDownload(getSelectedChapters()) R.id.action_download -> downloadChapters(getSelectedChapters())
R.id.action_delete -> onDelete(getSelectedChapters()) R.id.action_delete -> deleteChapters(getSelectedChapters())
else -> return false else -> return false
} }
return true return true
@ -294,66 +294,57 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
actionMode = null actionMode = null
} }
fun getSelectedChapters(): Observable<Chapter> { fun getSelectedChapters(): List<Chapter> {
val chapters = adapter.selectedItems.map { adapter.getItem(it) } return adapter.selectedItems.map { adapter.getItem(it) }
return Observable.from(chapters)
} }
fun destroyActionModeIfNeeded() { fun destroyActionModeIfNeeded() {
actionMode?.finish() actionMode?.finish()
} }
protected fun onSelectAll() { fun selectAll() {
adapter.selectAll() adapter.selectAll()
setContextTitle(adapter.selectedItemCount) setContextTitle(adapter.selectedItemCount)
} }
fun onMarkAsRead(chapters: Observable<Chapter>) { fun markAsRead(chapters: List<Chapter>) {
presenter.markChaptersRead(chapters, true) presenter.markChaptersRead(chapters, true)
if (presenter.preferences.removeAfterMarkedAsRead()) {
deleteChapters(chapters)
}
} }
fun onMarkAsUnread(chapters: Observable<Chapter>) { fun markAsUnread(chapters: List<Chapter>) {
presenter.markChaptersRead(chapters, false) presenter.markChaptersRead(chapters, false)
} }
fun onMarkPreviousAsRead(chapter: Chapter) { fun markPreviousAsRead(chapter: Chapter) {
presenter.markPreviousChaptersAsRead(chapter) presenter.markPreviousChaptersAsRead(chapter)
} }
fun onDownload(chapters: Observable<Chapter>) { fun downloadChapters(chapters: List<Chapter>) {
DownloadService.start(activity)
val observable = chapters.doOnCompleted { adapter.notifyDataSetChanged() }
presenter.downloadChapters(observable)
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
presenter.downloadChapters(chapters)
} }
fun onDelete(chapters: Observable<Chapter>) { fun deleteChapters(chapters: List<Chapter>) {
val size = adapter.selectedItemCount
val dialog = MaterialDialog.Builder(activity)
.title(R.string.deleting)
.progress(false, size, true)
.cancelable(false)
.show()
val observable = chapters
.concatMap { chapter ->
presenter.deleteChapter(chapter)
Observable.just(chapter)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { chapter ->
dialog.incrementProgress(1)
chapter.status = Download.NOT_DOWNLOADED
}
.doOnCompleted { adapter.notifyDataSetChanged() }
.doAfterTerminate { dialog.dismiss() }
presenter.deleteChapters(observable)
destroyActionModeIfNeeded() 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 { override fun onListItemClick(position: Int): Boolean {

View File

@ -10,14 +10,16 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import kotlinx.android.synthetic.main.item_chapter.view.* import kotlinx.android.synthetic.main.item_chapter.view.*
import rx.Observable
import java.text.DateFormat import java.text.DateFormat
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
import java.util.* import java.util.*
class ChaptersHolder(private val view: View, private val adapter: ChaptersAdapter, listener: FlexibleViewHolder.OnListItemClickListener) : class ChaptersHolder(
FlexibleViewHolder(view, adapter, listener) { 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 readColor = view.context.theme.getResourceColor(android.R.attr.textColorHint)
private val unreadColor = view.context.theme.getResourceColor(android.R.attr.textColorPrimary) 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 private var item: Chapter? = null
init { 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) { 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 // Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem -> popup.setOnMenuItemClickListener { menuItem ->
val chapter = Observable.just(item) val chapter = listOf(item)
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.action_download -> adapter.fragment.onDownload(chapter) R.id.action_download -> adapter.fragment.downloadChapters(chapter)
R.id.action_delete -> adapter.fragment.onDelete(chapter) R.id.action_delete -> adapter.fragment.deleteChapters(chapter)
R.id.action_mark_as_read -> adapter.fragment.onMarkAsRead(chapter) R.id.action_mark_as_read -> adapter.fragment.markAsRead(chapter)
R.id.action_mark_as_unread -> adapter.fragment.onMarkAsUnread(chapter) R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(chapter)
R.id.action_mark_previous_as_read -> adapter.fragment.onMarkPreviousAsRead(item) R.id.action_mark_previous_as_read -> adapter.fragment.markPreviousAsRead(item)
} }
true true
} }

View File

@ -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.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager 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.download.model.Download
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.base.Source import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.event.ChapterCountEvent import eu.kanade.tachiyomi.event.ChapterCountEvent
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.event.MangaEvent import eu.kanade.tachiyomi.event.MangaEvent
import eu.kanade.tachiyomi.event.ReaderEvent
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.SharedData import eu.kanade.tachiyomi.util.SharedData
import rx.Observable import rx.Observable
@ -163,50 +162,59 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
return db.getNextUnreadChapter(manga).executeAsBlocking() return db.getNextUnreadChapter(manga).executeAsBlocking()
} }
fun markChaptersRead(selectedChapters: Observable<Chapter>, read: Boolean) { fun markChaptersRead(selectedChapters: List<Chapter>, read: Boolean) {
add(selectedChapters.subscribeOn(Schedulers.io()) Observable.from(selectedChapters)
.doOnNext { chapter -> .doOnNext { chapter ->
chapter.read = read chapter.read = read
if (!read) chapter.last_page_read = 0 if (!read) {
chapter.last_page_read = 0
// Delete chapter when marked as read if desired by user.
if (preferences.removeAfterMarkedAsRead() && read) {
deleteChapter(chapter)
} }
} }
.toList() .toList()
.flatMap { chapters -> db.insertChapters(chapters).asRxObservable() } .flatMap { db.insertChapters(it).asRxObservable() }
.observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io())
.subscribe()) .subscribe()
} }
fun markPreviousChaptersAsRead(selected: Chapter) { fun markPreviousChaptersAsRead(selected: Chapter) {
Observable.from(chapters) Observable.from(chapters)
.filter { c -> c.chapter_number > -1 && c.chapter_number < selected.chapter_number } .filter { it.chapter_number > -1 && it.chapter_number < selected.chapter_number }
.doOnNext { c -> c.read = true } .doOnNext { it.read = true }
.toList() .toList()
.flatMap { chapters -> db.insertChapters(chapters).asRxObservable() } .flatMap { db.insertChapters(it).asRxObservable() }
.subscribe() .subscribe()
} }
fun downloadChapters(selectedChapters: Observable<Chapter>) { fun downloadChapters(chapters: List<Chapter>) {
add(selectedChapters.toList() DownloadService.start(context)
downloadManager.downloadChapters(manga, chapters)
}
fun deleteChapters(chapters: List<Chapter>) {
val wasRunning = downloadManager.isRunning
if (wasRunning) {
DownloadService.stop(context)
}
Observable.from(chapters)
.doOnNext { deleteChapter(it) }
.toList()
.doOnNext { if (onlyDownloaded()) refreshChapters() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { downloadManager.onDownloadChaptersEvent(DownloadChaptersEvent(manga, it)) }) .subscribeFirst({ view, result ->
view.onChaptersDeleted()
if (wasRunning) {
DownloadService.start(context)
}
}, { view, error ->
view.onChaptersDeletedError(error)
})
} }
fun deleteChapters(selectedChapters: Observable<Chapter>) { private fun deleteChapter(chapter: Chapter) {
add(selectedChapters.subscribe( downloadManager.queue.del(chapter)
{ chapter -> downloadManager.queue.del(chapter) },
{ error -> Timber.e(error.message) },
{
if (onlyDownloaded())
refreshChapters()
}))
}
fun deleteChapter(chapter: Chapter) {
downloadManager.deleteChapter(source, manga, chapter) downloadManager.deleteChapter(source, manga, chapter)
chapter.status = Download.NOT_DOWNLOADED
} }
fun revertSortOrder() { fun revertSortOrder() {

View File

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

View File

@ -1,14 +1,12 @@
package eu.kanade.tachiyomi.ui.recent package eu.kanade.tachiyomi.ui.recent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import eu.kanade.tachiyomi.R 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.database.models.MangaChapter
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.decoration.DividerItemDecoration 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.main.MainActivity
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.getResourceDrawable import eu.kanade.tachiyomi.util.getResourceDrawable
import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
import kotlinx.android.synthetic.main.fragment_recent_chapters.* import kotlinx.android.synthetic.main.fragment_recent_chapters.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
import rx.Observable import timber.log.Timber
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
/** /**
* Fragment that shows recent chapters. * Fragment that shows recent chapters.
@ -143,78 +140,57 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
} }
/** /**
* Start downloading chapter * Mark chapter as read
*
* @param chapters selected chapters * @param item selected chapter with manga
* @param manga manga that belongs to chapter
* @return true
*/ */
fun onDownload(chapters: Observable<Chapter>, manga: Manga): Boolean { fun markAsRead(item: MangaChapter) {
// Start the download service. presenter.markChapterRead(item.chapter, true)
DownloadService.start(activity) if (presenter.preferences.removeAfterMarkedAsRead()) {
deleteChapter(item)
}
}
// Refresh data on download competition. /**
val observable = chapters * Mark chapter as unread
.doOnCompleted({ *
adapter.notifyDataSetChanged() * @param item selected chapter with manga
presenter.start(presenter.CHAPTER_STATUS_CHANGES) */
}) fun markAsUnread(item: MangaChapter) {
presenter.markChapterRead(item.chapter, false)
}
// Download chapter. /**
presenter.downloadChapter(observable, manga) * Start downloading chapter
return true *
* @param item selected chapter with manga
*/
fun downloadChapter(item: MangaChapter) {
presenter.downloadChapter(item)
} }
/** /**
* Start deleting chapter * Start deleting chapter
* *
* @param chapters selected chapters * @param item selected chapter with manga
* @param manga manga that belongs to chapter
* @return success of deletion.
*/ */
fun onDelete(chapters: Observable<Chapter>, manga: Manga): Boolean { fun deleteChapter(item: MangaChapter) {
//Create observable DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
val observable = chapters presenter.deleteChapter(item)
.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 onChaptersDeleted() {
* Mark chapter as read dismissDeletingDialog()
adapter.notifyDataSetChanged()
* @param chapters selected chapter
* @return true
*/
fun onMarkAsRead(chapters: Observable<Chapter>, manga : Manga): Boolean {
// Set marked as read
presenter.markChaptersRead(chapters, manga, true)
return true
} }
/** fun onChaptersDeletedError(error: Throwable) {
* Mark chapter as unread dismissDeletingDialog()
Timber.e(error, error.message)
* @param chapters selected chapter
* @return true
*/
fun onMarkAsUnread(chapters: Observable<Chapter> , manga : Manga): Boolean {
// Set marked as unread
presenter.markChaptersRead(chapters, manga, false)
return true
} }
fun dismissDeletingDialog() {
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
}
} }

View File

@ -1,16 +1,13 @@
package eu.kanade.tachiyomi.ui.recent package eu.kanade.tachiyomi.ui.recent
import android.content.Context
import android.view.View import android.view.View
import android.widget.PopupMenu import android.widget.PopupMenu
import eu.kanade.tachiyomi.R 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.database.models.MangaChapter
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import kotlinx.android.synthetic.main.item_recent_chapter.view.* import kotlinx.android.synthetic.main.item_recent_chapter.view.*
import rx.Observable
/** /**
* Holder that contains chapter item * Holder that contains chapter item
@ -32,7 +29,7 @@ class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapte
/** /**
* Color of unread chapter * 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 * Object containing chapter information
@ -40,9 +37,10 @@ class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapte
private var mangaChapter: MangaChapter? = null private var mangaChapter: MangaChapter? = null
init { init {
//Set OnClickListener for download menu // We need to post a Runnable to show the popup to make sure that the PopupMenu is
itemView.chapterMenu.setOnClickListener { v -> v.post({ showPopupMenu(v) }) } // 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 // Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem -> popup.setOnMenuItemClickListener { menuItem ->
val chapterObservable = Observable.just<Chapter>(it.chapter)
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.action_download -> adapter.fragment.onDownload(chapterObservable, it.manga) R.id.action_download -> adapter.fragment.downloadChapter(it)
R.id.action_delete -> adapter.fragment.onDelete(chapterObservable, it.manga) R.id.action_delete -> adapter.fragment.deleteChapter(it)
R.id.action_mark_as_read -> adapter.fragment.onMarkAsRead(chapterObservable, it.manga); R.id.action_mark_as_read -> adapter.fragment.markAsRead(it)
R.id.action_mark_as_unread -> adapter.fragment.onMarkAsUnread(chapterObservable, it.manga); R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(it)
} }
false true
} }
} }

View File

@ -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.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.download.DownloadManager 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.download.model.Download
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -250,59 +250,69 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
} }
/** /**
* Download selected chapter * Mark selected chapter as read
* @param selectedChapter chapter that is selected *
* * * @param chapter selected chapter
* @param manga manga that belongs to chapter * @param read read status
*/ */
fun downloadChapter(selectedChapter: Observable<Chapter>, manga: Manga) { fun markChapterRead(chapter: Chapter, read: Boolean) {
add(selectedChapter.toList() Observable.just(chapter)
.observeOn(AndroidSchedulers.mainThread()) .doOnNext { chapter ->
.subscribe { downloadManager.onDownloadChaptersEvent(DownloadChaptersEvent(manga, it)) }) 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 * 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 chapter chapter that is selected
* *
* @param manga manga that belongs to chapter * @param manga manga that belongs to chapter
*/ */
fun deleteChapter(chapter: Chapter, manga: Manga) { private fun deleteChapter(chapter: Chapter, manga: Manga) {
val source = sourceManager.get(manga.source)!! val source = sourceManager.get(manga.source) ?: return
downloadManager.queue.del(chapter)
downloadManager.deleteChapter(source, manga, chapter) downloadManager.deleteChapter(source, manga, chapter)
chapter.status = Download.NOT_DOWNLOADED
} }
/**
* Delete selected chapter observable
* @param selectedChapters chapter that are selected
*/
fun deleteChapters(selectedChapters: Observable<Chapter>) {
add(selectedChapters
.subscribe(
{ chapter -> downloadManager.queue.del(chapter) })
{ error -> Timber.e(error.message) })
}
/**
* Mark selected chapter as read
* @param selectedChapters chapter that is selected
* *
* @param read read status
*/
fun markChaptersRead(selectedChapters: Observable<Chapter>, manga: Manga, read: Boolean) {
add(selectedChapters.subscribeOn(Schedulers.io())
.doOnNext { chapter ->
chapter.read = read
if (!read) chapter.last_page_read = 0
// Delete chapter when marked as read if desired by user.
if (preferences.removeAfterMarkedAsRead() && read) {
deleteChapter(chapter,manga)
}
}
.toList()
.flatMap { chapters -> db.insertChapters(chapters).asRxObservable() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe())
}
} }

View File

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