Merge pull request #350 from inorichi/dev

Rewrite DB models, tests and add a chapter loader.
This commit is contained in:
inorichi 2016-06-15 12:31:42 +02:00 committed by GitHub
commit 5c98e020f4
111 changed files with 2740 additions and 2924 deletions

View file

@ -84,27 +84,22 @@ android {
} }
dependencies { dependencies {
final SUPPORT_LIBRARY_VERSION = '23.4.0'
final DAGGER_VERSION = '2.4'
final RETROFIT_VERSION = '2.0.2'
final NUCLEUS_VERSION = '3.0.0'
final STORIO_VERSION = '1.8.0'
final MOCKITO_VERSION = '1.10.19'
// Modified dependencies // Modified dependencies
compile 'com.github.inorichi:subsampling-scale-image-view:421fb81' compile 'com.github.inorichi:subsampling-scale-image-view:421fb81'
compile 'com.github.inorichi:ReactiveNetwork:69092ed' compile 'com.github.inorichi:ReactiveNetwork:69092ed'
// Android support library // Android support library
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION" final support_library_version = '23.4.0'
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:support-v4:$support_library_version"
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:appcompat-v7:$support_library_version"
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:cardview-v7:$support_library_version"
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:design:$support_library_version"
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:recyclerview-v7:$support_library_version"
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:support-annotations:$support_library_version"
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:preference-v7:$support_library_version"
compile "com.android.support:customtabs:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:preference-v14:$support_library_version"
compile "com.android.support:customtabs:$support_library_version"
// ReactiveX // ReactiveX
compile 'io.reactivex:rxandroid:1.2.0' compile 'io.reactivex:rxandroid:1.2.0'
@ -115,15 +110,17 @@ dependencies {
compile "com.squareup.okhttp3:okhttp:3.3.1" compile "com.squareup.okhttp3:okhttp:3.3.1"
// REST // REST
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION" final retrofit_version = '2.0.2'
compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION" compile "com.squareup.retrofit2:retrofit:$retrofit_version"
compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION" compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// IO // IO
compile 'com.squareup.okio:okio:1.8.0' compile 'com.squareup.okio:okio:1.8.0'
// JSON // JSON
compile 'com.google.code.gson:gson:2.6.2' compile 'com.google.code.gson:gson:2.6.2'
compile 'com.github.salomonbrys.kotson:kotson:2.2.1'
// YAML // YAML
compile 'org.yaml:snakeyaml:1.17' compile 'org.yaml:snakeyaml:1.17'
@ -141,18 +138,18 @@ dependencies {
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database // Database
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION" final storio_version = '1.8.0'
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION" compile "com.pushtorefresh.storio:sqlite:$storio_version"
compile "com.pushtorefresh.storio:sqlite-annotations:$storio_version"
// Model View Presenter // Model View Presenter
compile "info.android15.nucleus:nucleus:$NUCLEUS_VERSION" final nucleus_version = '3.0.0'
compile "info.android15.nucleus:nucleus-support-v4:$NUCLEUS_VERSION" compile "info.android15.nucleus:nucleus:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v7:$NUCLEUS_VERSION" compile "info.android15.nucleus:nucleus-support-v4:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
// Dependency injection // Dependency injection
compile "com.google.dagger:dagger:$DAGGER_VERSION" compile "uy.kohesive.injekt:injekt-core:1.16.1"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
provided 'org.glassfish:javax.annotation:10.0-b28'
// Image library // Image library
compile 'com.github.bumptech.glide:glide:3.7.0' compile 'com.github.bumptech.glide:glide:3.7.0'
@ -174,13 +171,12 @@ dependencies {
// Tests // Tests
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'org.assertj:assertj-core:1.7.1'
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION" testCompile "org.mockito:mockito-core:1.10.19"
testCompile('org.robolectric:robolectric:3.0') { testCompile('org.robolectric:robolectric:3.0') {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.apache.httpcomponents', module: 'httpclient'
} }
kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
} }

View file

@ -3,12 +3,10 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.injection.AppComponentFactory
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector
import eu.kanade.tachiyomi.injection.component.AppComponent
import org.acra.ACRA import org.acra.ACRA
import org.acra.annotation.ReportsCrashes import org.acra.annotation.ReportsCrashes
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt
@ReportsCrashes( @ReportsCrashes(
formUri = "http://tachiyomi.kanade.eu/crash_report", formUri = "http://tachiyomi.kanade.eu/crash_report",
@ -19,22 +17,13 @@ import timber.log.Timber
) )
open class App : Application() { open class App : Application() {
lateinit var component: AppComponent
private set
lateinit var componentReflection: ComponentReflectionInjector<AppComponent>
private set
var appTheme = 0 var appTheme = 0
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Injekt.importModule(AppModule(this))
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
component = createAppComponent()
componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
setupTheme() setupTheme()
setupAcra() setupAcra()
} }
@ -43,10 +32,6 @@ open class App : Application() {
appTheme = PreferencesHelper.getTheme(this) appTheme = PreferencesHelper.getTheme(this)
} }
protected open fun createAppComponent(): AppComponent {
return AppComponentFactory.create(this)
}
protected open fun setupAcra() { protected open fun setupAcra() {
ACRA.init(this) ACRA.init(this)
} }

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi
import android.app.Application
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingletonFactory
class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory { PreferencesHelper(app) }
addSingletonFactory { DatabaseHelper(app) }
addSingletonFactory { ChapterCache(app) }
addSingletonFactory { CoverCache(app) }
addSingletonFactory { NetworkHelper(app) }
addSingletonFactory { SourceManager(app) }
addSingletonFactory { DownloadManager(app) }
addSingletonFactory { MangaSyncManager(app) }
addSingletonFactory { Gson() }
}
}

View file

@ -1,14 +1,13 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.* import com.google.gson.*
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.* import eu.kanade.tachiyomi.data.database.models.*
import java.io.* import java.io.*
import java.lang.reflect.Type
import java.util.* import java.util.*
/** /**
@ -191,8 +190,7 @@ class BackupManager(private val db: DatabaseHelper) {
private fun restoreCategories(jsonCategories: JsonArray) { private fun restoreCategories(jsonCategories: JsonArray) {
// Get categories from file and from db // Get categories from file and from db
val dbCategories = db.getCategories().executeAsBlocking() val dbCategories = db.getCategories().executeAsBlocking()
val backupCategories = getArrayOrEmpty<Category>(jsonCategories, val backupCategories = gson.fromJson<List<CategoryImpl>>(jsonCategories)
object : TypeToken<List<Category>>() {}.type)
// Iterate over them // Iterate over them
for (category in backupCategories) { for (category in backupCategories) {
@ -224,17 +222,13 @@ class BackupManager(private val db: DatabaseHelper) {
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json. * @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
*/ */
private fun restoreMangas(jsonMangas: JsonArray) { private fun restoreMangas(jsonMangas: JsonArray) {
val chapterToken = object : TypeToken<List<Chapter>>() {}.type
val mangaSyncToken = object : TypeToken<List<MangaSync>>() {}.type
val categoriesNamesToken = object : TypeToken<List<String>>() {}.type
for (backupManga in jsonMangas) { for (backupManga in jsonMangas) {
// Map every entry to objects // Map every entry to objects
val element = backupManga.asJsonObject val element = backupManga.asJsonObject
val manga = gson.fromJson(element.get(MANGA), Manga::class.java) val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
val chapters = getArrayOrEmpty<Chapter>(element.get(CHAPTERS), chapterToken) val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
val sync = getArrayOrEmpty<MangaSync>(element.get(MANGA_SYNC), mangaSyncToken) val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray())
val categories = getArrayOrEmpty<String>(element.get(CATEGORIES), categoriesNamesToken) val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
// Restore everything related to this manga // Restore everything related to this manga
restoreManga(manga) restoreManga(manga)
@ -340,7 +334,7 @@ class BackupManager(private val db: DatabaseHelper) {
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) { private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
// Fix foreign keys with the current manga id // Fix foreign keys with the current manga id
for (mangaSync in sync) { for (mangaSync in sync) {
mangaSync.manga_id = manga.id mangaSync.manga_id = manga.id!!
} }
val dbSyncs = db.getMangasSync(manga).executeAsBlocking() val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
@ -367,15 +361,4 @@ class BackupManager(private val db: DatabaseHelper) {
} }
} }
/**
* Returns a list of items from a json element, or an empty list if the element is null.
*
* @param element the json to be mapped to a list of items.
* @param type the gson mapping to restore the list.
* @return a list of items.
*/
private fun <T> getArrayOrEmpty(element: JsonElement?, type: Type): List<T> {
return gson.fromJson<List<T>>(element, type) ?: ArrayList<T>()
}
} }

View file

@ -10,6 +10,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
@ -44,7 +45,7 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
class CategoryGetResolver : DefaultGetResolver<Category>() { class CategoryGetResolver : DefaultGetResolver<Category>() {
override fun mapFromCursor(cursor: Cursor) = Category().apply { override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
id = cursor.getInt(cursor.getColumnIndex(COL_ID)) id = cursor.getInt(cursor.getColumnIndex(COL_ID))
name = cursor.getString(cursor.getColumnIndex(COL_NAME)) name = cursor.getString(cursor.getColumnIndex(COL_NAME))
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER)) order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))

View file

@ -10,6 +10,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_CHAPTER_NUMBER import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_CHAPTER_NUMBER
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_FETCH import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_FETCH
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_UPLOAD import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_UPLOAD
@ -56,7 +57,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
class ChapterGetResolver : DefaultGetResolver<Chapter>() { class ChapterGetResolver : DefaultGetResolver<Chapter>() {
override fun mapFromCursor(cursor: Cursor) = Chapter().apply { override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)) manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
url = cursor.getString(cursor.getColumnIndex(COL_URL)) url = cursor.getString(cursor.getColumnIndex(COL_URL))

View file

@ -44,7 +44,7 @@ class HistoryPutResolver : DefaultPutResolver<History>() {
class HistoryGetResolver : DefaultGetResolver<History>() { class HistoryGetResolver : DefaultGetResolver<History>() {
override fun mapFromCursor(cursor: Cursor) = History().apply { override fun mapFromCursor(cursor: Cursor): History = History().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID)) chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ)) last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))

View file

@ -42,7 +42,7 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() { class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
override fun mapFromCursor(cursor: Cursor) = MangaCategory().apply { override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)) manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID)) category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))

View file

@ -10,6 +10,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID
@ -54,7 +55,7 @@ class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() { class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
override fun mapFromCursor(cursor: Cursor) = MangaSync().apply { override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)) manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID)) sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))

View file

@ -10,6 +10,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
@ -66,7 +67,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
open class MangaGetResolver : DefaultGetResolver<Manga>() { open class MangaGetResolver : DefaultGetResolver<Manga>() {
override fun mapFromCursor(cursor: Cursor) = Manga().apply { override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
source = cursor.getInt(cursor.getColumnIndex(COL_SOURCE)) source = cursor.getInt(cursor.getColumnIndex(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndex(COL_URL)) url = cursor.getString(cursor.getColumnIndex(COL_URL))

View file

@ -1,57 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
@StorIOSQLiteType(table = CategoryTable.TABLE)
public class Category implements Serializable {
@StorIOSQLiteColumn(name = CategoryTable.COL_ID, key = true)
public Integer id;
@StorIOSQLiteColumn(name = CategoryTable.COL_NAME)
public String name;
@StorIOSQLiteColumn(name = CategoryTable.COL_ORDER)
public int order;
@StorIOSQLiteColumn(name = CategoryTable.COL_FLAGS)
public int flags;
public Category() {}
public static Category create(String name) {
Category c = new Category();
c.name = name;
return c;
}
public static Category createDefault() {
Category c = create("Default");
c.id = 0;
return c;
}
public String getNameLower() {
return name.toLowerCase();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Category category = (Category) o;
return name.equals(category.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}

View file

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface Category : Serializable {
var id: Int?
var name: String
var order: Int
var flags: Int
val nameLower: String
get() = name.toLowerCase()
companion object {
fun create(name: String): Category = CategoryImpl().apply {
this.name = name
}
fun createDefault(): Category = create("Default").apply { id = 0 }
}
}

View file

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.data.database.models
class CategoryImpl : Category {
override var id: Int? = null
override lateinit var name: String
override var order: Int = 0
override var flags: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val category = other as Category
return name == category.name
}
override fun hashCode(): Int {
return name.hashCode()
}
}

View file

@ -1,94 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import java.util.List;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.UrlUtil;
@StorIOSQLiteType(table = ChapterTable.TABLE)
public class Chapter implements Serializable {
@StorIOSQLiteColumn(name = ChapterTable.COL_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = ChapterTable.COL_MANGA_ID)
public Long manga_id;
@StorIOSQLiteColumn(name = ChapterTable.COL_URL)
public String url;
@StorIOSQLiteColumn(name = ChapterTable.COL_NAME)
public String name;
@StorIOSQLiteColumn(name = ChapterTable.COL_READ)
public boolean read;
@StorIOSQLiteColumn(name = ChapterTable.COL_LAST_PAGE_READ)
public int last_page_read;
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_FETCH)
public long date_fetch;
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_UPLOAD)
public long date_upload;
@StorIOSQLiteColumn(name = ChapterTable.COL_CHAPTER_NUMBER)
public float chapter_number;
@StorIOSQLiteColumn(name = ChapterTable.COL_SOURCE_ORDER)
public int source_order;
public int status;
private transient List<Page> pages;
public Chapter() {}
public void setUrl(String url) {
this.url = UrlUtil.getPath(url);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Chapter chapter = (Chapter) o;
return url.equals(chapter.url);
}
@Override
public int hashCode() {
return url.hashCode();
}
public static Chapter create() {
Chapter chapter = new Chapter();
chapter.chapter_number = -1;
return chapter;
}
public List<Page> getPages() {
return pages;
}
public void setPages(List<Page> pages) {
this.pages = pages;
}
public boolean isDownloaded() {
return status == Download.DOWNLOADED;
}
public boolean isRecognizedNumber() {
return chapter_number >= 0f;
}
}

View file

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface Chapter : Serializable {
var id: Long?
var manga_id: Long?
var url: String
var name: String
var read: Boolean
var last_page_read: Int
var date_fetch: Long
var date_upload: Long
var chapter_number: Float
var source_order: Int
val isRecognizedNumber: Boolean
get() = chapter_number >= 0f
companion object {
fun create(): Chapter = ChapterImpl().apply {
chapter_number = -1f
}
}
}

View file

@ -0,0 +1,39 @@
package eu.kanade.tachiyomi.data.database.models
class ChapterImpl : Chapter {
override var id: Long? = null
override var manga_id: Long? = null
override lateinit var url: String
override lateinit var name: String
override var read: Boolean = false
override var last_page_read: Int = 0
override var date_fetch: Long = 0
override var date_upload: Long = 0
override var chapter_number: Float = 0f
override var source_order: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter
return url == chapter.url
}
override fun hashCode(): Int {
return url.hashCode()
}
}

View file

@ -1,58 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import eu.kanade.tachiyomi.data.database.tables.HistoryTable;
/**
* Object containing the history statistics of a chapter
*/
@StorIOSQLiteType(table = HistoryTable.TABLE)
public class History implements Serializable {
/**
* Id of history object.
*/
@StorIOSQLiteColumn(name = HistoryTable.COL_ID, key = true)
public Long id;
/**
* Chapter id of history object.
*/
@StorIOSQLiteColumn(name = HistoryTable.COL_CHAPTER_ID)
public long chapter_id;
/**
* Last time chapter was read in time long format
*/
@StorIOSQLiteColumn(name = HistoryTable.COL_LAST_READ)
public long last_read;
/**
* Total time chapter was read - todo not yet implemented
*/
@StorIOSQLiteColumn(name = HistoryTable.COL_TIME_READ)
public long time_read;
/**
* Empty history constructor
*/
public History() {
}
/**
* History constructor
*
* @param chapter chapter object
* @return history object
*/
public static History create(Chapter chapter) {
History history = new History();
history.chapter_id = chapter.id;
return history;
}
}

View file

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
/**
* Object containing the history statistics of a chapter
*/
class History : Serializable {
/**
* Id of history object.
*/
var id: Long? = null
/**
* Chapter id of history object.
*/
var chapter_id: Long = 0
/**
* Last time chapter was read in time long format
*/
var last_read: Long = 0
/**
* Total time chapter was read - todo not yet implemented
*/
var time_read: Long = 0
companion object {
/**
* History constructor
*
* @param chapter chapter object
* @return history object
*/
fun create(chapter: Chapter): History {
val history = History()
history.chapter_id = chapter.id!!
return history
}
}
}

View file

@ -1,213 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import android.content.Context;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
import eu.kanade.tachiyomi.util.UrlUtil;
@StorIOSQLiteType(table = MangaTable.TABLE)
public class Manga implements Serializable {
@StorIOSQLiteColumn(name = MangaTable.COL_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = MangaTable.COL_SOURCE)
public int source;
@StorIOSQLiteColumn(name = MangaTable.COL_URL)
public String url;
@StorIOSQLiteColumn(name = MangaTable.COL_ARTIST)
public String artist;
@StorIOSQLiteColumn(name = MangaTable.COL_AUTHOR)
public String author;
@StorIOSQLiteColumn(name = MangaTable.COL_DESCRIPTION)
public String description;
@StorIOSQLiteColumn(name = MangaTable.COL_GENRE)
public String genre;
@StorIOSQLiteColumn(name = MangaTable.COL_TITLE)
public String title;
@StorIOSQLiteColumn(name = MangaTable.COL_STATUS)
public int status;
@StorIOSQLiteColumn(name = MangaTable.COL_THUMBNAIL_URL)
public String thumbnail_url;
@StorIOSQLiteColumn(name = MangaTable.COL_FAVORITE)
public boolean favorite;
@StorIOSQLiteColumn(name = MangaTable.COL_LAST_UPDATE)
public long last_update;
@StorIOSQLiteColumn(name = MangaTable.COL_INITIALIZED)
public boolean initialized;
@StorIOSQLiteColumn(name = MangaTable.COL_VIEWER)
public int viewer;
@StorIOSQLiteColumn(name = MangaTable.COL_CHAPTER_FLAGS)
public int chapter_flags;
public transient int unread;
public transient int category;
public static final int UNKNOWN = 0;
public static final int ONGOING = 1;
public static final int COMPLETED = 2;
public static final int LICENSED = 3;
public static final int SORT_DESC = 0x00000000;
public static final int SORT_ASC = 0x00000001;
public static final int SORT_MASK = 0x00000001;
// Generic filter that does not filter anything
public static final int SHOW_ALL = 0x00000000;
public static final int SHOW_UNREAD = 0x00000002;
public static final int SHOW_READ = 0x00000004;
public static final int READ_MASK = 0x00000006;
public static final int SHOW_DOWNLOADED = 0x00000008;
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
public static final int DOWNLOADED_MASK = 0x00000018;
public static final int SORTING_SOURCE = 0x00000000;
public static final int SORTING_NUMBER = 0x00000100;
public static final int SORTING_MASK = 0x00000100;
public static final int DISPLAY_NAME = 0x00000000;
public static final int DISPLAY_NUMBER = 0x00100000;
public static final int DISPLAY_MASK = 0x00100000;
public Manga() {}
public static Manga create(String pathUrl) {
Manga m = new Manga();
m.url = pathUrl;
return m;
}
public static Manga create(String pathUrl, int source) {
Manga m = new Manga();
m.url = pathUrl;
m.source = source;
return m;
}
public void setUrl(String url) {
this.url = UrlUtil.getPath(url);
}
public void copyFrom(Manga other) {
if (other.title != null)
title = other.title;
if (other.author != null)
author = other.author;
if (other.artist != null)
artist = other.artist;
if (other.url != null)
url = other.url;
if (other.description != null)
description = other.description;
if (other.genre != null)
genre = other.genre;
if (other.thumbnail_url != null)
thumbnail_url = other.thumbnail_url;
status = other.status;
initialized = true;
}
public String getStatus(Context context) {
switch (status) {
case ONGOING:
return context.getString(R.string.ongoing);
case COMPLETED:
return context.getString(R.string.completed);
case LICENSED:
return context.getString(R.string.licensed);
default:
return context.getString(R.string.unknown);
}
}
public void setChapterOrder(int order) {
setFlags(order, SORT_MASK);
}
public void setDisplayMode(int mode) {
setFlags(mode, DISPLAY_MASK);
}
public void setReadFilter(int filter) {
setFlags(filter, READ_MASK);
}
public void setDownloadedFilter(int filter) {
setFlags(filter, DOWNLOADED_MASK);
}
public void setSorting(int sort) {
setFlags(sort, SORTING_MASK);
}
private void setFlags(int flag, int mask) {
chapter_flags = (chapter_flags & ~mask) | (flag & mask);
}
public boolean sortDescending() {
return (chapter_flags & SORT_MASK) == SORT_DESC;
}
// Used to display the chapter's title one way or another
public int getDisplayMode() {
return chapter_flags & DISPLAY_MASK;
}
public int getReadFilter() {
return chapter_flags & READ_MASK;
}
public int getDownloadedFilter() {
return chapter_flags & DOWNLOADED_MASK;
}
public int getSorting() {
return chapter_flags & SORTING_MASK;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Manga manga = (Manga) o;
return url.equals(manga.url);
}
@Override
public int hashCode() {
return url.hashCode();
}
}

View file

@ -0,0 +1,131 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface Manga : Serializable {
var id: Long?
var source: Int
var url: String
var title: String
var artist: String?
var author: String?
var description: String?
var genre: String?
var status: Int
var thumbnail_url: String?
var favorite: Boolean
var last_update: Long
var initialized: Boolean
var viewer: Int
var chapter_flags: Int
var unread: Int
var category: Int
fun copyFrom(other: Manga) {
if (other.author != null)
author = other.author
if (other.artist != null)
artist = other.artist
if (other.description != null)
description = other.description
if (other.genre != null)
genre = other.genre
if (other.thumbnail_url != null)
thumbnail_url = other.thumbnail_url
status = other.status
initialized = true
}
fun setChapterOrder(order: Int) {
setFlags(order, SORT_MASK)
}
private fun setFlags(flag: Int, mask: Int) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
fun sortDescending(): Boolean {
return chapter_flags and SORT_MASK == SORT_DESC
}
// Used to display the chapter's title one way or another
var displayMode: Int
get() = chapter_flags and DISPLAY_MASK
set(mode) = setFlags(mode, DISPLAY_MASK)
var readFilter: Int
get() = chapter_flags and READ_MASK
set(filter) = setFlags(filter, READ_MASK)
var downloadedFilter: Int
get() = chapter_flags and DOWNLOADED_MASK
set(filter) = setFlags(filter, DOWNLOADED_MASK)
var sorting: Int
get() = chapter_flags and SORTING_MASK
set(sort) = setFlags(sort, SORTING_MASK)
companion object {
const val UNKNOWN = 0
const val ONGOING = 1
const val COMPLETED = 2
const val LICENSED = 3
const val SORT_DESC = 0x00000000
const val SORT_ASC = 0x00000001
const val SORT_MASK = 0x00000001
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000
const val SHOW_UNREAD = 0x00000002
const val SHOW_READ = 0x00000004
const val READ_MASK = 0x00000006
const val SHOW_DOWNLOADED = 0x00000008
const val SHOW_NOT_DOWNLOADED = 0x00000010
const val DOWNLOADED_MASK = 0x00000018
const val SORTING_SOURCE = 0x00000000
const val SORTING_NUMBER = 0x00000100
const val SORTING_MASK = 0x00000100
const val DISPLAY_NAME = 0x00000000
const val DISPLAY_NUMBER = 0x00100000
const val DISPLAY_MASK = 0x00100000
fun create(source: Int): Manga = MangaImpl().apply {
this.source = source
}
fun create(pathUrl: String, source: Int = 0): Manga = MangaImpl().apply {
url = pathUrl
this.source = source
}
}
}

View file

@ -1,29 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
@StorIOSQLiteType(table = MangaCategoryTable.TABLE)
public class MangaCategory {
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_MANGA_ID)
public long manga_id;
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_CATEGORY_ID)
public int category_id;
public MangaCategory() {}
public static MangaCategory create(Manga manga, Category category) {
MangaCategory mc = new MangaCategory();
mc.manga_id = manga.id;
mc.category_id = category.id;
return mc;
}
}

View file

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.data.database.models
class MangaCategory {
var id: Long? = null
var manga_id: Long = 0
var category_id: Int = 0
companion object {
fun create(manga: Manga, category: Category): MangaCategory {
val mc = MangaCategory()
mc.manga_id = manga.id!!
mc.category_id = category.id!!
return mc
}
}
}

View file

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
public class MangaChapter {
public Manga manga;
public Chapter chapter;
public MangaChapter(Manga manga, Chapter chapter) {
this.manga = manga;
this.chapter = chapter;
}
}

View file

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.data.database.models
class MangaChapter(val manga: Manga, val chapter: Chapter)

View file

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
/**
* Object containing manga, chapter and history
*/
public class MangaChapterHistory {
/**
* Object containing manga and chapter
*/
public MangaChapter mangaChapter;
/**
* Object containing history
*/
public History history;
/**
* MangaChapterHistory constructor
*
* @param mangaChapter object containing manga and chapter
* @param history object containing history
*/
public MangaChapterHistory(MangaChapter mangaChapter, History history) {
this.mangaChapter = mangaChapter;
this.history = history;
}
}

View file

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.data.database.models
/**
* Object containing manga, chapter and history
*
* @param manga object containing manga
* @param chapter object containing chater
* @param history object containing history
*/
class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)

View file

@ -0,0 +1,53 @@
package eu.kanade.tachiyomi.data.database.models
class MangaImpl : Manga {
override var id: Long? = null
override var source: Int = 0
override lateinit var url: String
override lateinit var title: String
override var artist: String? = null
override var author: String? = null
override var description: String? = null
override var genre: String? = null
override var status: Int = 0
override var thumbnail_url: String? = null
override var favorite: Boolean = false
override var last_update: Long = 0
override var initialized: Boolean = false
override var viewer: Int = 0
override var chapter_flags: Int = 0
@Transient override var unread: Int = 0
@Transient override var category: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val manga = other as Manga
return url == manga.url
}
override fun hashCode(): Int {
return url.hashCode()
}
}

View file

@ -1,78 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService;
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
public class MangaSync implements Serializable {
@StorIOSQLiteColumn(name = MangaSyncTable.COL_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = MangaSyncTable.COL_MANGA_ID)
public long manga_id;
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SYNC_ID)
public int sync_id;
@StorIOSQLiteColumn(name = MangaSyncTable.COL_REMOTE_ID)
public int remote_id;
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TITLE)
public String title;
@StorIOSQLiteColumn(name = MangaSyncTable.COL_LAST_CHAPTER_READ)
public int last_chapter_read;
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TOTAL_CHAPTERS)
public int total_chapters;
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SCORE)
public float score;
@StorIOSQLiteColumn(name = MangaSyncTable.COL_STATUS)
public int status;
public boolean update;
public static MangaSync create() {
return new MangaSync();
}
public static MangaSync create(MangaSyncService service) {
MangaSync mangasync = new MangaSync();
mangasync.sync_id = service.getId();
return mangasync;
}
public void copyPersonalFrom(MangaSync other) {
last_chapter_read = other.last_chapter_read;
score = other.score;
status = other.status;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MangaSync mangaSync = (MangaSync) o;
if (manga_id != mangaSync.manga_id) return false;
if (sync_id != mangaSync.sync_id) return false;
return remote_id == mangaSync.remote_id;
}
@Override
public int hashCode() {
int result = (int) (manga_id ^ (manga_id >>> 32));
result = 31 * result + sync_id;
result = 31 * result + remote_id;
return result;
}
}

View file

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface MangaSync : Serializable {
var id: Long?
var manga_id: Long
var sync_id: Int
var remote_id: Int
var title: String
var last_chapter_read: Int
var total_chapters: Int
var score: Float
var status: Int
var update: Boolean
fun copyPersonalFrom(other: MangaSync) {
last_chapter_read = other.last_chapter_read
score = other.score
status = other.status
}
companion object {
fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply {
sync_id = serviceId
}
}
}

View file

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.data.database.models
class MangaSyncImpl : MangaSync {
override var id: Long? = null
override var manga_id: Long = 0
override var sync_id: Int = 0
override var remote_id: Int = 0
override lateinit var title: String
override var last_chapter_read: Int = 0
override var total_chapters: Int = 0
override var score: Float = 0f
override var status: Int = 0
override var update: Boolean = false
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val mangaSync = other as MangaSync
if (manga_id != mangaSync.manga_id) return false
if (sync_id != mangaSync.sync_id) return false
return remote_id == mangaSync.remote_id
}
override fun hashCode(): Int {
var result = (manga_id xor manga_id.ushr(32)).toInt()
result = 31 * result + sync_id
result = 31 * result + remote_id
return result
}
}

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.database.queries package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
@ -34,80 +33,6 @@ interface ChapterQueries : DbProvider {
.withGetResolver(MangaChapterGetResolver.INSTANCE) .withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare() .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.COL_MANGA_ID} = ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} > ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} <= ?")
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
.orderBy(ChapterTable.COL_CHAPTER_NUMBER)
.limit(1)
.build())
.prepare()
}
fun getNextChapterBySource(chapter: Chapter) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("""${ChapterTable.COL_MANGA_ID} = ? AND
${ChapterTable.COL_SOURCE_ORDER} < ?""")
.whereArgs(chapter.manga_id, chapter.source_order)
.orderBy("${ChapterTable.COL_SOURCE_ORDER} DESC")
.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.COL_MANGA_ID} = ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} < ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} >= ?")
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
.orderBy("${ChapterTable.COL_CHAPTER_NUMBER} DESC")
.limit(1)
.build())
.prepare()
}
fun getPreviousChapterBySource(chapter: Chapter) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("""${ChapterTable.COL_MANGA_ID} = ? AND
${ChapterTable.COL_SOURCE_ORDER} > ?""")
.whereArgs(chapter.manga_id, chapter.source_order)
.orderBy(ChapterTable.COL_SOURCE_ORDER)
.limit(1)
.build())
.prepare()
fun getNextUnreadChapter(manga: Manga) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
"${ChapterTable.COL_READ} = ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} >= ?")
.whereArgs(manga.id, 0, 0)
.orderBy(ChapterTable.COL_CHAPTER_NUMBER)
.limit(1)
.build())
.prepare()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare() fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare() fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()

View file

@ -5,7 +5,6 @@ import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() { class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
@ -46,10 +45,7 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl")) manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
chapter.id = history.chapter_id chapter.id = history.chapter_id
// Create mangaChapter object
val mangaChapter = MangaChapter(manga, chapter)
// Return result // Return result
return MangaChapterHistory(mangaChapter, history) return MangaChapterHistory(manga, chapter, history)
} }
} }

View file

@ -16,10 +16,7 @@ import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.util.DiskUtils import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
import eu.kanade.tachiyomi.util.UrlUtil
import eu.kanade.tachiyomi.util.saveImageTo
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -27,12 +24,17 @@ import rx.schedulers.Schedulers
import rx.subjects.BehaviorSubject import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import java.io.FileReader import java.io.FileReader
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) { class DownloadManager(
private val context: Context,
private val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) {
private val gson = Gson() private val gson = Gson()
@ -270,10 +272,8 @@ class DownloadManager(private val context: Context, private val sourceManager: S
} }
page page
} }
.retryWhen { // Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
it.zipWith(Observable.range(1, 3)) { errors, retries -> retries } .retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }))
.flatMap { retries -> Observable.timer((retries * 2).toLong(), TimeUnit.SECONDS) }
}
} }
// Public method to get the image from the filesystem. It does NOT provide any way to download the image // Public method to get the image from the filesystem. It does NOT provide any way to download the image

View file

@ -7,14 +7,13 @@ import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
class DownloadService : Service() { class DownloadService : Service() {
@ -29,8 +28,8 @@ class DownloadService : Service() {
} }
} }
@Inject lateinit var downloadManager: DownloadManager val downloadManager: DownloadManager by injectLazy()
@Inject lateinit var preferences: PreferencesHelper val preferences: PreferencesHelper by injectLazy()
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var networkChangeSubscription: Subscription? = null private var networkChangeSubscription: Subscription? = null
@ -39,7 +38,6 @@ class DownloadService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
App.get(this).component.inject(this)
createWakeLock() createWakeLock()

View file

@ -7,28 +7,26 @@ import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.GlideModule import com.bumptech.glide.module.GlideModule
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.NetworkHelper import eu.kanade.tachiyomi.data.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject
/** /**
* Class used to update Glide module settings * Class used to update Glide module settings
*/ */
class AppGlideModule : GlideModule { class AppGlideModule : GlideModule {
@Inject lateinit var networkHelper: NetworkHelper
override fun applyOptions(context: Context, builder: GlideBuilder) { override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB // Set the cache size of Glide to 15 MiB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024)) builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
} }
override fun registerComponents(context: Context, glide: Glide) { override fun registerComponents(context: Context, glide: Glide) {
App.get(context).component.inject(this) val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
glide.register(GlideUrl::class.java, InputStream::class.java,
OkHttpUrlLoader.Factory(networkHelper.client)) glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory)
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
} }
} }

View file

@ -5,14 +5,13 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.* import com.bumptech.glide.load.model.*
import com.bumptech.glide.load.model.stream.StreamModelLoader import com.bumptech.glide.load.model.stream.StreamModelLoader
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject
/** /**
* A class for loading a cover associated with a [Manga] that can be present in our own cache. * A class for loading a cover associated with a [Manga] that can be present in our own cache.
@ -30,12 +29,12 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
/** /**
* Cover cache where persistent covers are stored. * Cover cache where persistent covers are stored.
*/ */
@Inject lateinit var coverCache: CoverCache val coverCache: CoverCache by injectLazy()
/** /**
* Source manager. * Source manager.
*/ */
@Inject lateinit var sourceManager: SourceManager val sourceManager: SourceManager by injectLazy()
/** /**
* Base network loader. * Base network loader.
@ -54,10 +53,6 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
*/ */
private val cachedHeaders = hashMapOf<Int, LazyHeaders>() private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
init {
App.get(context).component.inject(this)
}
/** /**
* Factory class for creating [MangaModelLoader] instances. * Factory class for creating [MangaModelLoader] instances.
*/ */
@ -88,7 +83,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
// Obtain the request url and the file for this url from the LRU cache, or calculate it // Obtain the request url and the file for this url from the LRU cache, or calculate it
// and add them to the cache. // and add them to the cache.
val (glideUrl, file) = modelCache.get(url, width, height) ?: val (glideUrl, file) = modelCache.get(url, width, height) ?:
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url)).apply { Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url!!)).apply {
modelCache.put(url, width, height, this) modelCache.put(url, width, height, this)
} }

View file

@ -10,12 +10,12 @@ import android.os.PowerManager
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
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.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
@ -24,9 +24,9 @@ import eu.kanade.tachiyomi.util.*
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
/** /**
* This class will take care of updating the chapters of the manga from the library. It can be * This class will take care of updating the chapters of the manga from the library. It can be
@ -41,17 +41,17 @@ class LibraryUpdateService : Service() {
/** /**
* Database helper. * Database helper.
*/ */
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
/** /**
* Source manager. * Source manager.
*/ */
@Inject lateinit var sourceManager: SourceManager val sourceManager: SourceManager by injectLazy()
/** /**
* Preferences. * Preferences.
*/ */
@Inject lateinit var preferences: PreferencesHelper val preferences: PreferencesHelper by injectLazy()
/** /**
* Wake lock that will be held until the service is destroyed. * Wake lock that will be held until the service is destroyed.
@ -126,7 +126,6 @@ class LibraryUpdateService : Service() {
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
App.get(this).component.inject(this)
createAndAcquireWakeLock() createAndAcquireWakeLock()
} }

View file

@ -2,23 +2,18 @@ package eu.kanade.tachiyomi.data.mangasync
import android.content.Context import android.content.Context
import android.support.annotation.CallSuper import android.support.annotation.CallSuper
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.network.NetworkHelper import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import rx.Completable import rx.Completable
import rx.Observable import rx.Observable
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
abstract class MangaSyncService(private val context: Context, val id: Int) { abstract class MangaSyncService(private val context: Context, val id: Int) {
@Inject lateinit var preferences: PreferencesHelper val preferences: PreferencesHelper by injectLazy()
@Inject lateinit var networkService: NetworkHelper val networkService: NetworkHelper by injectLazy()
init {
App.get(context).component.inject(this)
}
open val client: OkHttpClient open val client: OkHttpClient
get() = networkService.client get() = networkService.client

View file

@ -4,25 +4,23 @@ import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.MangaSync
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
class UpdateMangaSyncService : Service() { class UpdateMangaSyncService : Service() {
@Inject lateinit var syncManager: MangaSyncManager val syncManager: MangaSyncManager by injectLazy()
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
private lateinit var subscriptions: CompositeSubscription private lateinit var subscriptions: CompositeSubscription
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
App.get(this).component.inject(this)
subscriptions = CompositeSubscription() subscriptions = CompositeSubscription()
} }

View file

@ -97,8 +97,8 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
.flatMap { Observable.from(it.select("entry")) } .flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" } .filter { it.select("type").text() != "Novel" }
.map { .map {
MangaSync.create(this).apply { MangaSync.create(id).apply {
title = it.selectText("title") title = it.selectText("title")!!
remote_id = it.selectInt("id") remote_id = it.selectInt("id")
total_chapters = it.selectInt("chapters") total_chapters = it.selectInt("chapters")
} }
@ -114,8 +114,8 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
.map { Jsoup.parse(it.body().string()) } .map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("manga")) } .flatMap { Observable.from(it.select("manga")) }
.map { .map {
MangaSync.create(this).apply { MangaSync.create(id).apply {
title = it.selectText("series_title") title = it.selectText("series_title")!!
remote_id = it.selectInt("series_mangadb_id") remote_id = it.selectInt("series_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters") last_chapter_read = it.selectInt("my_read_chapters")
status = it.selectInt("my_status") status = it.selectInt("my_status")

View file

@ -54,8 +54,6 @@ class PreferenceKeys(context: Context) {
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key) val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
val seamlessMode = context.getString(R.string.pref_seamless_mode_key)
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list) val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
val enabledLanguages = context.getString(R.string.pref_source_languages) val enabledLanguages = context.getString(R.string.pref_source_languages)

View file

@ -101,8 +101,6 @@ class PreferencesHelper(private val context: Context) {
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0) fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false) fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN")) fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN"))

View file

@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.data.source.model; package eu.kanade.tachiyomi.data.source.model;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.network.ProgressListener; import eu.kanade.tachiyomi.data.network.ProgressListener;
import eu.kanade.tachiyomi.ui.reader.ReaderChapter;
import rx.subjects.PublishSubject; import rx.subjects.PublishSubject;
public class Page implements ProgressListener { public class Page implements ProgressListener {
@ -11,7 +9,7 @@ public class Page implements ProgressListener {
private int pageNumber; private int pageNumber;
private String url; private String url;
private String imageUrl; private String imageUrl;
private transient Chapter chapter; private transient ReaderChapter chapter;
private transient String imagePath; private transient String imagePath;
private transient volatile int status; private transient volatile int status;
private transient volatile int progress; private transient volatile int progress;
@ -90,16 +88,12 @@ public class Page implements ProgressListener {
this.statusSubject = subject; this.statusSubject = subject;
} }
public Chapter getChapter() { public ReaderChapter getChapter() {
return chapter; return chapter;
} }
public void setChapter(Chapter chapter) { public void setChapter(ReaderChapter chapter) {
this.chapter = chapter; this.chapter = chapter;
} }
public boolean isLastPage() {
List<Page> chapterPages = chapter.getPages();
return chapterPages.size() -1 == pageNumber;
}
} }

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.source.online package eu.kanade.tachiyomi.data.source.online
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
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
@ -14,9 +13,10 @@ import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.UrlUtil
import okhttp3.* import okhttp3.*
import rx.Observable import rx.Observable
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
/** /**
* A simple implementation for sources from a website. * A simple implementation for sources from a website.
@ -28,17 +28,17 @@ abstract class OnlineSource(context: Context) : Source {
/** /**
* Network service. * Network service.
*/ */
@Inject lateinit var network: NetworkHelper val network: NetworkHelper by injectLazy()
/** /**
* Chapter cache. * Chapter cache.
*/ */
@Inject lateinit var chapterCache: ChapterCache val chapterCache: ChapterCache by injectLazy()
/** /**
* Preferences helper. * Preferences helper.
*/ */
@Inject lateinit var preferences: PreferencesHelper val preferences: PreferencesHelper by injectLazy()
/** /**
* Base url of the website without the trailing slash, like: http://mysite.com * Base url of the website without the trailing slash, like: http://mysite.com
@ -61,11 +61,6 @@ abstract class OnlineSource(context: Context) : Source {
open val client: OkHttpClient open val client: OkHttpClient
get() = network.client get() = network.client
init {
// Inject dependencies.
App.get(context).component.inject(this)
}
/** /**
* Headers builder for requests. Implementations can override this method for custom headers. * Headers builder for requests. Implementations can override this method for custom headers.
*/ */
@ -443,6 +438,15 @@ abstract class OnlineSource(context: Context) : Source {
} }
} }
fun Chapter.setUrlWithoutDomain(url: String) {
this.url = UrlUtil.getPath(url)
}
fun Manga.setUrlWithoutDomain(url: String) {
this.url = UrlUtil.getPath(url)
}
// Overridable method to allow custom parsing. // Overridable method to allow custom parsing.
open fun parseChapterNumber(chapter: Chapter) { open fun parseChapterNumber(chapter: Chapter) {

View file

@ -26,8 +26,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
override fun popularMangaParse(response: Response, page: MangasPage) { override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string()) val document = Jsoup.parse(response.body().string())
for (element in document.select(popularMangaSelector())) { for (element in document.select(popularMangaSelector())) {
Manga().apply { Manga.create(id).apply {
source = this@ParsedOnlineSource.id
popularMangaFromElement(element, this) popularMangaFromElement(element, this)
page.mangas.add(this) page.mangas.add(this)
} }
@ -70,8 +69,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
override fun searchMangaParse(response: Response, page: MangasPage, query: String) { override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
val document = Jsoup.parse(response.body().string()) val document = Jsoup.parse(response.body().string())
for (element in document.select(searchMangaSelector())) { for (element in document.select(searchMangaSelector())) {
Manga().apply { Manga.create(id).apply {
source = this@ParsedOnlineSource.id
searchMangaFromElement(element, this) searchMangaFromElement(element, this)
page.mangas.add(this) page.mangas.add(this)
} }

View file

@ -54,10 +54,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
override fun popularMangaParse(response: Response, page: MangasPage) { override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string()) val document = Jsoup.parse(response.body().string())
for (element in document.select(map.popular.manga_css)) { for (element in document.select(map.popular.manga_css)) {
Manga().apply { Manga.create(id).apply {
source = this@YamlOnlineSource.id
title = element.text() title = element.text()
setUrl(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
page.mangas.add(this) page.mangas.add(this)
} }
} }
@ -84,10 +83,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
override fun searchMangaParse(response: Response, page: MangasPage, query: String) { override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
val document = Jsoup.parse(response.body().string()) val document = Jsoup.parse(response.body().string())
for (element in document.select(map.search.manga_css)) { for (element in document.select(map.search.manga_css)) {
Manga().apply { Manga.create(id).apply {
source = this@YamlOnlineSource.id
title = element.text() title = element.text()
setUrl(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
page.mangas.add(this) page.mangas.add(this)
} }
} }
@ -123,7 +121,7 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
val chapter = Chapter.create() val chapter = Chapter.create()
element.select(title).first().let { element.select(title).first().let {
chapter.name = it.text() chapter.name = it.text()
chapter.setUrl(it.attr("href")) chapter.setUrlWithoutDomain(it.attr("href"))
} }
val dateElement = element.select(date?.select).first() val dateElement = element.select(date?.select).first()
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0 chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0

View file

@ -62,8 +62,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun popularMangaParse(response: Response, page: MangasPage) { override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string()) val document = Jsoup.parse(response.body().string())
for (element in document.select(popularMangaSelector())) { for (element in document.select(popularMangaSelector())) {
Manga().apply { Manga.create(id).apply {
source = this@Batoto.id
popularMangaFromElement(element, this) popularMangaFromElement(element, this)
page.mangas.add(this) page.mangas.add(this)
} }
@ -78,7 +77,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun popularMangaFromElement(element: Element, manga: Manga) { override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("a[href^=http://bato.to]").first().let { element.select("a[href^=http://bato.to]").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text().trim() manga.title = it.text().trim()
} }
} }
@ -90,8 +89,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun searchMangaParse(response: Response, page: MangasPage, query: String) { override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
val document = Jsoup.parse(response.body().string()) val document = Jsoup.parse(response.body().string())
for (element in document.select(searchMangaSelector())) { for (element in document.select(searchMangaSelector())) {
Manga().apply { Manga.create(id).apply {
source = this@Batoto.id
searchMangaFromElement(element, this) searchMangaFromElement(element, this)
page.mangas.add(this) page.mangas.add(this)
} }
@ -156,7 +154,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun chapterFromElement(element: Element, chapter: Chapter) { override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a[href^=http://bato.to/reader").first() val urlElement = element.select("a[href^=http://bato.to/reader").first()
chapter.setUrl(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = element.select("td").getOrNull(4)?.let { chapter.date_upload = element.select("td").getOrNull(4)?.let {
parseDateFromElement(it) parseDateFromElement(it)

View file

@ -35,7 +35,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaFromElement(element: Element, manga: Manga) { override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("td a:eq(0)").first().let { element.select("td a:eq(0)").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
} }
@ -88,7 +88,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun chapterFromElement(element: Element, chapter: Chapter) { override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first() val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("MM/dd/yyyy").parse(it).time SimpleDateFormat("MM/dd/yyyy").parse(it).time

View file

@ -29,7 +29,7 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
override fun popularMangaFromElement(element: Element, manga: Manga) { override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("a.title").first().let { element.select("a.title").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
} }
@ -43,7 +43,7 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element, manga: Manga) {
element.select("a.series_preview").first().let { element.select("a.series_preview").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
} }
@ -74,7 +74,7 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
override fun chapterFromElement(element: Element, chapter: Chapter) { override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a.tips").first() val urlElement = element.select("a.tips").first()
chapter.setUrl(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
} }

View file

@ -27,7 +27,7 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaFromElement(element: Element, manga: Manga) { override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("div.title > a").first().let { element.select("div.title > a").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
} }
@ -41,7 +41,7 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element, manga: Manga) {
element.select("a.manga_info").first().let { element.select("a.manga_info").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
} }
@ -71,7 +71,7 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun chapterFromElement(element: Element, chapter: Chapter) { override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first() val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
} }

View file

@ -28,7 +28,7 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
override fun popularMangaFromElement(element: Element, manga: Manga) { override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("div.title > h2 > a").first().let { element.select("div.title > h2 > a").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
} }
} }
@ -54,7 +54,7 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element, manga: Manga) {
element.select("div.title > h2 > a").first().let { element.select("div.title > h2 > a").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
} }
} }
@ -83,7 +83,7 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
override fun chapterFromElement(element: Element, chapter: Chapter) { override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first() val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.select("span.val").text() chapter.name = urlElement.select("span.val").text()
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
} }

View file

@ -29,7 +29,7 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaFromElement(element: Element, manga: Manga) { override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("h2 > a").first().let { element.select("h2 > a").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
} }
@ -69,7 +69,7 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun chapterFromElement(element: Element, chapter: Chapter) { override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first() val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = element.select("div.date").first()?.text()?.let { chapter.date_upload = element.select("div.date").first()?.text()?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time

View file

@ -30,7 +30,7 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaFromElement(element: Element, manga: Manga) { override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("h3 > a").first().let { element.select("h3 > a").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
} }
} }
@ -69,7 +69,7 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun chapterFromElement(element: Element, chapter: Chapter) { override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first() val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href") + "?mature=1") chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
chapter.name = urlElement.text().replace(" новое", "") chapter.name = urlElement.text().replace(" новое", "")
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time

View file

@ -30,7 +30,7 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaFromElement(element: Element, manga: Manga) { override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("h3 > a").first().let { element.select("h3 > a").first().let {
manga.setUrl(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
} }
} }
@ -69,7 +69,7 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun chapterFromElement(element: Element, chapter: Chapter) { override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first() val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href") + "?mature=1") chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
chapter.name = urlElement.text().replace(" новое", "") chapter.name = urlElement.text().replace(" новое", "")
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time

View file

@ -8,7 +8,6 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.AsyncTask import android.os.AsyncTask
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.network.GET import eu.kanade.tachiyomi.data.network.GET
@ -18,8 +17,8 @@ import eu.kanade.tachiyomi.data.network.newCallWithProgress
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.saveTo import eu.kanade.tachiyomi.util.saveTo
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import javax.inject.Inject
class UpdateDownloader(private val context: Context) : class UpdateDownloader(private val context: Context) :
AsyncTask<String, Int, UpdateDownloader.DownloadResult>() { AsyncTask<String, Int, UpdateDownloader.DownloadResult>() {
@ -40,7 +39,7 @@ class UpdateDownloader(private val context: Context) :
} }
} }
@Inject lateinit var network: NetworkHelper val network: NetworkHelper by injectLazy()
/** /**
* Default download dir * Default download dir
@ -59,9 +58,6 @@ class UpdateDownloader(private val context: Context) :
private val notificationId: Int private val notificationId: Int
get() = Constants.NOTIFICATION_UPDATER_ID get() = Constants.NOTIFICATION_UPDATER_ID
init {
App.get(context).component.inject(this)
}
/** /**
* Class containing download result * Class containing download result

View file

@ -1,16 +0,0 @@
package eu.kanade.tachiyomi.injection;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.injection.component.AppComponent;
import eu.kanade.tachiyomi.injection.component.DaggerAppComponent;
import eu.kanade.tachiyomi.injection.module.AppModule;
public class AppComponentFactory {
public static AppComponent create(App app) {
return DaggerAppComponent.builder().appModule(new AppModule(app)).build();
}
}

View file

@ -1,97 +0,0 @@
package eu.kanade.tachiyomi.injection;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
* This class allows to inject into objects through a base class,
* so we don't have to repeat injection code everywhere.
*
* The performance drawback is about 0.013 ms per injection on a very slow device,
* which is negligible in most cases.
*
* Example:
* <pre>{@code
* Component {
* void inject(B b);
* }
*
* class A {
* void onCreate() {
* componentReflectionInjector.inject(this);
* }
* }
*
* class B extends A {
* @Inject MyDependency dependency;
* }
*
* new B().onCreate() // dependency will be injected at this point
*
* class C extends B {
*
* }
*
* new C().onCreate() // dependency will be injected at this point as well
* }</pre>
*
* @param <T> a type of dagger 2 component.
*/
public final class ComponentReflectionInjector<T> {
private static final ConcurrentHashMap<Class<?>, HashMap<Class<?>, Method>> cache = new ConcurrentHashMap<>();
private final Class<T> componentClass;
private final T component;
private final HashMap<Class<?>, Method> methods;
public ComponentReflectionInjector(Class<T> componentClass, T component) {
this.componentClass = componentClass;
this.component = component;
this.methods = getMethods(componentClass);
}
public T getComponent() {
return component;
}
public void inject(Object target) {
Class targetClass = target.getClass();
Method method = methods.get(targetClass);
while (method == null && targetClass != null) {
targetClass = targetClass.getSuperclass();
method = methods.get(targetClass);
}
if (method == null)
throw new RuntimeException(String.format("No %s injecting method exists in %s component", target.getClass(), componentClass));
try {
method.invoke(component, target);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
private static HashMap<Class<?>, Method> getMethods(Class componentClass) {
HashMap<Class<?>, Method> methods = cache.get(componentClass);
if (methods == null) {
synchronized (cache) {
methods = cache.get(componentClass);
if (methods == null) {
methods = new HashMap<>();
for (Method method : componentClass.getMethods()) {
Class<?>[] params = method.getParameterTypes();
if (params.length == 1)
methods.put(params[0], method);
}
cache.put(componentClass, methods);
}
}
}
return methods;
}
}

View file

@ -1,67 +0,0 @@
package eu.kanade.tachiyomi.injection.component
import android.app.Application
import dagger.Component
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.glide.AppGlideModule
import eu.kanade.tachiyomi.data.glide.MangaModelLoader
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.updater.UpdateDownloader
import eu.kanade.tachiyomi.injection.module.AppModule
import eu.kanade.tachiyomi.injection.module.DataModule
import eu.kanade.tachiyomi.ui.backup.BackupPresenter
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
import eu.kanade.tachiyomi.ui.download.DownloadPresenter
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListPresenter
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersPresenter
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadPresenter
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
import javax.inject.Singleton
@Singleton
@Component(modules = arrayOf(AppModule::class, DataModule::class))
interface AppComponent {
fun inject(libraryPresenter: LibraryPresenter)
fun inject(mangaPresenter: MangaPresenter)
fun inject(cataloguePresenter: CataloguePresenter)
fun inject(mangaInfoPresenter: MangaInfoPresenter)
fun inject(chaptersPresenter: ChaptersPresenter)
fun inject(readerPresenter: ReaderPresenter)
fun inject(downloadPresenter: DownloadPresenter)
fun inject(myAnimeListPresenter: MyAnimeListPresenter)
fun inject(categoryPresenter: CategoryPresenter)
fun inject(recentChaptersPresenter: RecentChaptersPresenter)
fun inject(recentlyReadPresenter: RecentlyReadPresenter)
fun inject(backupPresenter: BackupPresenter)
fun inject(mainActivity: MainActivity)
fun inject(settingsActivity: SettingsActivity)
fun inject(source: Source)
fun inject(mangaSyncService: MangaSyncService)
fun inject(onlineSource: OnlineSource)
fun inject(libraryUpdateService: LibraryUpdateService)
fun inject(downloadService: DownloadService)
fun inject(updateMangaSyncService: UpdateMangaSyncService)
fun inject(mangaModelLoader: MangaModelLoader)
fun inject(appGlideModule: AppGlideModule)
fun inject(updateDownloader: UpdateDownloader)
fun application(): Application
}

View file

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.injection.module
import android.app.Application
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
/**
* Provide application-level dependencies. Mainly singleton object that can be injected from
* anywhere in the app.
*/
@Module
class AppModule(private val application: Application) {
@Provides
@Singleton
fun provideApplication(): Application {
return application
}
}

View file

@ -1,70 +0,0 @@
package eu.kanade.tachiyomi.injection.module
import android.app.Application
import dagger.Module
import dagger.Provides
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import javax.inject.Singleton
/**
* Provide dependencies to the DataManager, mainly Helper classes and Retrofit services.
*/
@Module
open class DataModule {
@Provides
@Singleton
fun providePreferencesHelper(app: Application): PreferencesHelper {
return PreferencesHelper(app)
}
@Provides
@Singleton
open fun provideDatabaseHelper(app: Application): DatabaseHelper {
return DatabaseHelper(app)
}
@Provides
@Singleton
fun provideChapterCache(app: Application): ChapterCache {
return ChapterCache(app)
}
@Provides
@Singleton
fun provideCoverCache(app: Application): CoverCache {
return CoverCache(app)
}
@Provides
@Singleton
open fun provideNetworkHelper(app: Application): NetworkHelper {
return NetworkHelper(app)
}
@Provides
@Singleton
open fun provideSourceManager(app: Application): SourceManager {
return SourceManager(app)
}
@Provides
@Singleton
fun provideDownloadManager(app: Application, sourceManager: SourceManager, preferences: PreferencesHelper): DownloadManager {
return DownloadManager(app, sourceManager, preferences)
}
@Provides
@Singleton
fun provideMangaSyncManager(app: Application): MangaSyncManager {
return MangaSyncManager(app)
}
}

View file

@ -8,9 +8,9 @@ import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject
/** /**
* Presenter of [BackupFragment]. * Presenter of [BackupFragment].
@ -20,7 +20,7 @@ class BackupPresenter : BasePresenter<BackupFragment>() {
/** /**
* Database. * Database.
*/ */
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
/** /**
* Backup manager. * Backup manager.

View file

@ -12,7 +12,6 @@ abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P
setPresenterFactory { setPresenterFactory {
superFactory.createPresenter().apply { superFactory.createPresenter().apply {
val app = application as App val app = application as App
app.componentReflection.inject(this)
context = app.applicationContext context = app.applicationContext
} }
} }

View file

@ -12,7 +12,6 @@ abstract class BaseRxFragment<P : BasePresenter<*>> : NucleusSupportFragment<P>(
setPresenterFactory { setPresenterFactory {
superFactory.createPresenter().apply { superFactory.createPresenter().apply {
val app = activity.application as App val app = activity.application as App
app.componentReflection.inject(this)
context = app.applicationContext context = app.applicationContext
} }
} }

View file

@ -56,7 +56,7 @@ class CatalogueAdapter(val fragment: CatalogueFragment) : FlexibleAdapter<Catalo
* @return an identifier for the item. * @return an identifier for the item.
*/ */
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return mItems[position].id return mItems[position].id!!
} }
/** /**

View file

@ -383,7 +383,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
* @return the holder of the manga or null if it's not bound. * @return the holder of the manga or null if it's not bound.
*/ */
private fun getHolder(manga: Manga): CatalogueGridHolder? { private fun getHolder(manga: Manga): CatalogueGridHolder? {
return catalogue_grid.findViewHolderForItemId(manga.id) as? CatalogueGridHolder return catalogue_grid.findViewHolderForItemId(manga.id!!) as? CatalogueGridHolder
} }
/** /**

View file

@ -19,7 +19,7 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
/** /**
* Presenter of [CatalogueFragment]. * Presenter of [CatalogueFragment].
@ -29,22 +29,22 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
/** /**
* Source manager. * Source manager.
*/ */
@Inject lateinit var sourceManager: SourceManager val sourceManager: SourceManager by injectLazy()
/** /**
* Database. * Database.
*/ */
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
/** /**
* Preferences. * Preferences.
*/ */
@Inject lateinit var prefs: PreferencesHelper val prefs: PreferencesHelper by injectLazy()
/** /**
* Cover cache. * Cover cache.
*/ */
@Inject lateinit var coverCache: CoverCache val coverCache: CoverCache by injectLazy()
/** /**
* Enabled sources. * Enabled sources.

View file

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
/** /**
* Presenter of CategoryActivity. * Presenter of CategoryActivity.
@ -17,7 +17,7 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
/** /**
* Used to connect to database * Used to connect to database
*/ */
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
/** /**
* List containing categories * List containing categories

View file

@ -35,7 +35,7 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHo
* @return an identifier for the item. * @return an identifier for the item.
*/ */
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return getItem(position).chapter.id return getItem(position).chapter.id!!
} }
/** /**

View file

@ -262,7 +262,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
* @return the holder of the download or null if it's not bound. * @return the holder of the download or null if it's not bound.
*/ */
private fun getHolder(download: Download): DownloadHolder? { private fun getHolder(download: Download): DownloadHolder? {
return recycler.findViewHolderForItemId(download.chapter.id) as? DownloadHolder return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
} }
/** /**

View file

@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
/** /**
* Presenter of [DownloadFragment]. * Presenter of [DownloadFragment].
@ -24,7 +24,7 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
/** /**
* Download manager. * Download manager.
*/ */
@Inject lateinit var downloadManager: DownloadManager val downloadManager: DownloadManager by injectLazy()
/** /**
* Property to get the queue from the download manager. * Property to get the queue from the download manager.

View file

@ -49,7 +49,7 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
* @return an identifier for the item. * @return an identifier for the item.
*/ */
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return mItems[position].id return mItems[position].id!!
} }
/** /**
@ -72,8 +72,8 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
* @return true if the manga should be included, false otherwise. * @return true if the manga should be included, false otherwise.
*/ */
override fun filterObject(manga: Manga, query: String): Boolean = with(manga) { override fun filterObject(manga: Manga, query: String): Boolean = with(manga) {
title != null && title.toLowerCase().contains(query) || title.toLowerCase().contains(query) ||
author != null && author.toLowerCase().contains(query) author != null && author!!.toLowerCase().contains(query)
} }
/** /**

View file

@ -16,10 +16,10 @@ import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subjects.BehaviorSubject import rx.subjects.BehaviorSubject
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
import javax.inject.Inject
/** /**
* Presenter of [LibraryFragment]. * Presenter of [LibraryFragment].
@ -49,27 +49,27 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
/** /**
* Database. * Database.
*/ */
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
/** /**
* Preferences. * Preferences.
*/ */
@Inject lateinit var preferences: PreferencesHelper val preferences: PreferencesHelper by injectLazy()
/** /**
* Cover cache. * Cover cache.
*/ */
@Inject lateinit var coverCache: CoverCache val coverCache: CoverCache by injectLazy()
/** /**
* Source manager. * Source manager.
*/ */
@Inject lateinit var sourceManager: SourceManager val sourceManager: SourceManager by injectLazy()
/** /**
* Download manager. * Download manager.
*/ */
@Inject lateinit var downloadManager: DownloadManager val downloadManager: DownloadManager by injectLazy()
companion object { companion object {
/** /**
@ -279,7 +279,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
@Throws(IOException::class) @Throws(IOException::class)
fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean { fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
if (manga.thumbnail_url != null && manga.favorite) { if (manga.thumbnail_url != null && manga.favorite) {
coverCache.copyToCache(manga.thumbnail_url, inputStream) coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
return true return true
} }
return false return false

View file

@ -5,7 +5,6 @@ import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v4.view.GravityCompat import android.support.v4.view.GravityCompat
import android.view.MenuItem import android.view.MenuItem
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.backup.BackupFragment import eu.kanade.tachiyomi.ui.backup.BackupFragment
@ -18,11 +17,11 @@ import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment
import eu.kanade.tachiyomi.ui.setting.SettingsActivity import eu.kanade.tachiyomi.ui.setting.SettingsActivity
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
@Inject lateinit var preferences: PreferencesHelper val preferences: PreferencesHelper by injectLazy()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
setAppTheme() setAppTheme()
@ -34,8 +33,6 @@ class MainActivity : BaseActivity() {
return return
} }
App.get(this).component.inject(this)
// Inflate activity_main.xml. // Inflate activity_main.xml.
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)

View file

@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
import eu.kanade.tachiyomi.util.SharedData import eu.kanade.tachiyomi.util.SharedData
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
/** /**
* Presenter of [MangaActivity]. * Presenter of [MangaActivity].
@ -19,12 +19,12 @@ class MangaPresenter : BasePresenter<MangaActivity>() {
/** /**
* Database helper. * Database helper.
*/ */
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
/** /**
* Manga sync manager. * Manga sync manager.
*/ */
@Inject lateinit var syncManager: MangaSyncManager val syncManager: MangaSyncManager by injectLazy()
/** /**
* Manga associated with this instance. * Manga associated with this instance.

View file

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.ui.manga.chapter
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.download.model.Download
class ChapterModel(c: Chapter) : Chapter by c {
private var _status: Int = 0
var status: Int
get() = download?.status ?: _status
set(value) { _status = value }
var download: Download? = null
val isDownloaded: Boolean
get() = status == Download.DOWNLOADED
}

View file

@ -3,10 +3,9 @@ package eu.kanade.tachiyomi.ui.manga.chapter
import android.view.ViewGroup import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter 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.util.inflate import eu.kanade.tachiyomi.util.inflate
class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter<ChaptersHolder, Chapter>() { class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter<ChaptersHolder, ChapterModel>() {
init { init {
setHasStableIds(true) setHasStableIds(true)
@ -30,10 +29,10 @@ class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter<Chapters
} }
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return mItems[position].id return mItems[position].id!!
} }
fun setItems(chapters: List<Chapter>) { fun setItems(chapters: List<ChapterModel>) {
mItems = chapters mItems = chapters
notifyDataSetChanged() notifyDataSetChanged()
} }

View file

@ -134,8 +134,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
presenter.setDownloadedFilter(item.isChecked) presenter.setDownloadedFilter(item.isChecked)
} }
R.id.action_filter_empty -> { R.id.action_filter_empty -> {
presenter.setReadFilter(false) presenter.removeFilters()
presenter.setDownloadedFilter(false)
activity.supportInvalidateOptionsMenu() activity.supportInvalidateOptionsMenu()
} }
R.id.action_sort -> presenter.revertSortOrder() R.id.action_sort -> presenter.revertSortOrder()
@ -150,7 +149,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
setDownloadedFilter() setDownloadedFilter()
} }
fun onNextChapters(chapters: List<Chapter>) { fun onNextChapters(chapters: List<ChapterModel>) {
// If the list is empty, fetch chapters from source if the conditions are met // If the list is empty, fetch chapters from source if the conditions are met
// We use presenter chapters instead because they are always unfiltered // We use presenter chapters instead because they are always unfiltered
if (presenter.chapters.isEmpty()) if (presenter.chapters.isEmpty())
@ -206,7 +205,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
// Save the new display mode // Save the new display mode
presenter.setDisplayMode(itemView.id) presenter.setDisplayMode(itemView.id)
// Refresh ui // Refresh ui
adapter.notifyDataSetChanged() adapter.notifyItemRangeChanged(0, adapter.itemCount)
true true
} }
.show() .show()
@ -271,7 +270,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
} }
private fun getHolder(chapter: Chapter): ChaptersHolder? { private fun getHolder(chapter: Chapter): ChaptersHolder? {
return recycler.findViewHolderForItemId(chapter.id) as? ChaptersHolder return recycler.findViewHolderForItemId(chapter.id!!) as? ChaptersHolder
} }
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
@ -309,7 +308,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
actionMode = null actionMode = null
} }
fun getSelectedChapters(): List<Chapter> { fun getSelectedChapters(): List<ChapterModel> {
return adapter.selectedItems.map { adapter.getItem(it) } return adapter.selectedItems.map { adapter.getItem(it) }
} }
@ -322,27 +321,27 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
setContextTitle(adapter.selectedItemCount) setContextTitle(adapter.selectedItemCount)
} }
fun markAsRead(chapters: List<Chapter>) { fun markAsRead(chapters: List<ChapterModel>) {
presenter.markChaptersRead(chapters, true) presenter.markChaptersRead(chapters, true)
if (presenter.preferences.removeAfterMarkedAsRead()) { if (presenter.preferences.removeAfterMarkedAsRead()) {
deleteChapters(chapters) deleteChapters(chapters)
} }
} }
fun markAsUnread(chapters: List<Chapter>) { fun markAsUnread(chapters: List<ChapterModel>) {
presenter.markChaptersRead(chapters, false) presenter.markChaptersRead(chapters, false)
} }
fun markPreviousAsRead(chapter: Chapter) { fun markPreviousAsRead(chapter: ChapterModel) {
presenter.markPreviousChaptersAsRead(chapter) presenter.markPreviousChaptersAsRead(chapter)
} }
fun downloadChapters(chapters: List<Chapter>) { fun downloadChapters(chapters: List<ChapterModel>) {
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
presenter.downloadChapters(chapters) presenter.downloadChapters(chapters)
} }
fun deleteChapters(chapters: List<Chapter>) { fun deleteChapters(chapters: List<ChapterModel>) {
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
presenter.deleteChapters(chapters) presenter.deleteChapters(chapters)
@ -350,7 +349,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
fun onChaptersDeleted() { fun onChaptersDeleted() {
dismissDeletingDialog() dismissDeletingDialog()
adapter.notifyDataSetChanged() adapter.notifyItemRangeChanged(0, adapter.itemCount)
} }
fun onChaptersDeletedError(error: Throwable) { fun onChaptersDeletedError(error: Throwable) {

View file

@ -1,10 +1,8 @@
package eu.kanade.tachiyomi.ui.manga.chapter package eu.kanade.tachiyomi.ui.manga.chapter
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.Manga import eu.kanade.tachiyomi.data.database.models.Manga
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
@ -26,7 +24,7 @@ class ChaptersHolder(
private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' }) private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' })
private val df = DateFormat.getDateInstance(DateFormat.SHORT) private val df = DateFormat.getDateInstance(DateFormat.SHORT)
private var item: Chapter? = null private var item: ChapterModel? = null
init { init {
// We need to post a Runnable to show the popup to make sure that the PopupMenu is // We need to post a Runnable to show the popup to make sure that the PopupMenu is
@ -35,19 +33,16 @@ class ChaptersHolder(
view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
} }
fun onSetValues(chapter: Chapter, manga: Manga?) = with(view) { fun onSetValues(chapter: ChapterModel, manga: Manga?) = with(view) {
item = chapter item = chapter
val name: String chapter_title.text = when (manga?.displayMode) {
when (manga?.displayMode) {
Manga.DISPLAY_NUMBER -> { Manga.DISPLAY_NUMBER -> {
val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble()) val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble())
name = context.getString(R.string.display_mode_chapter, formattedNumber) context.getString(R.string.display_mode_chapter, formattedNumber)
} }
else -> name = chapter.name else -> chapter.name
} }
chapter_title.text = name
chapter_title.setTextColor(if (chapter.read) readColor else unreadColor) chapter_title.setTextColor(if (chapter.read) readColor else unreadColor)
if (chapter.date_upload > 0) { if (chapter.date_upload > 0) {
@ -57,31 +52,26 @@ class ChaptersHolder(
chapter_date.text = "" chapter_date.text = ""
} }
if (!chapter.read && chapter.last_page_read > 0) { chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) {
chapter_pages.text = context.getString(R.string.chapter_progress, chapter.last_page_read + 1) context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
} else { } else {
chapter_pages.text = "" ""
} }
notifyStatus(chapter.status) notifyStatus(chapter.status)
} }
fun notifyStatus(status: Int) = with(view) { fun notifyStatus(status: Int) = with(view.download_text) {
when (status) { when (status) {
Download.QUEUE -> download_text.setText(R.string.chapter_queued) Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> download_text.setText(R.string.chapter_downloading) Download.DOWNLOADING -> setText(R.string.chapter_downloading)
Download.DOWNLOADED -> download_text.setText(R.string.chapter_downloaded) Download.DOWNLOADED -> setText(R.string.chapter_downloaded)
Download.ERROR -> download_text.setText(R.string.chapter_error) Download.ERROR -> setText(R.string.chapter_error)
else -> download_text.text = "" else -> text = ""
} }
} }
fun onProgressChange(context: Context, downloaded: Int, total: Int) { private fun showPopupMenu(view: View) = item?.let { chapter ->
view.download_text.text = context.getString(
R.string.chapter_downloading_progress, downloaded, total)
}
private fun showPopupMenu(view: View) = item?.let { item ->
// Create a PopupMenu, giving it the clicked view for an anchor // Create a PopupMenu, giving it the clicked view for an anchor
val popup = PopupMenu(view.context, view) val popup = PopupMenu(view.context, view)
@ -89,32 +79,35 @@ class ChaptersHolder(
popup.menuInflater.inflate(R.menu.chapter_single, popup.menu) popup.menuInflater.inflate(R.menu.chapter_single, popup.menu)
// Hide download and show delete if the chapter is downloaded // Hide download and show delete if the chapter is downloaded
if (item.isDownloaded) { if (chapter.isDownloaded) {
popup.menu.findItem(R.id.action_download).isVisible = false popup.menu.findItem(R.id.action_download).isVisible = false
popup.menu.findItem(R.id.action_delete).isVisible = true popup.menu.findItem(R.id.action_delete).isVisible = true
} }
// Hide mark as unread when the chapter is unread // Hide mark as unread when the chapter is unread
if (!item.read && item.last_page_read == 0) { if (!chapter.read && chapter.last_page_read == 0) {
popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false
} }
// Hide mark as read when the chapter is read // Hide mark as read when the chapter is read
if (item.read) { if (chapter.read) {
popup.menu.findItem(R.id.action_mark_as_read).isVisible = false popup.menu.findItem(R.id.action_mark_as_read).isVisible = false
} }
// 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 = listOf(item) val chapterList = listOf(chapter)
when (menuItem.itemId) { with(adapter.fragment) {
R.id.action_download -> adapter.fragment.downloadChapters(chapter) when (menuItem.itemId) {
R.id.action_delete -> adapter.fragment.deleteChapters(chapter) R.id.action_download -> downloadChapters(chapterList)
R.id.action_mark_as_read -> adapter.fragment.markAsRead(chapter) R.id.action_delete -> deleteChapters(chapterList)
R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(chapter) R.id.action_mark_as_read -> markAsRead(chapterList)
R.id.action_mark_previous_as_read -> adapter.fragment.markPreviousAsRead(item) R.id.action_mark_as_unread -> markAsUnread(chapterList)
R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter)
}
} }
true true
} }

View file

@ -20,108 +20,197 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
/**
* Presenter of [ChaptersFragment].
*/
class ChaptersPresenter : BasePresenter<ChaptersFragment>() { class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
@Inject lateinit var db: DatabaseHelper /**
@Inject lateinit var sourceManager: SourceManager * Database helper.
@Inject lateinit var preferences: PreferencesHelper */
@Inject lateinit var downloadManager: DownloadManager val db: DatabaseHelper by injectLazy()
/**
* Source manager.
*/
val sourceManager: SourceManager by injectLazy()
/**
* Preferences.
*/
val preferences: PreferencesHelper by injectLazy()
/**
* Downloads manager.
*/
val downloadManager: DownloadManager by injectLazy()
/**
* Active manga.
*/
lateinit var manga: Manga lateinit var manga: Manga
private set private set
/**
* Source of the manga.
*/
lateinit var source: Source lateinit var source: Source
private set private set
lateinit var chapters: List<Chapter> /**
* List of chapters of the manga. It's always unfiltered and unsorted.
*/
lateinit var chapters: List<ChapterModel>
private set private set
lateinit var chaptersSubject: PublishSubject<List<Chapter>> /**
* Subject of list of chapters to allow updating the view without going to DB.
*/
val chaptersSubject by lazy { PublishSubject.create<List<ChapterModel>>() }
/**
* Whether the chapter list has been requested to the source.
*/
var hasRequested = false
private set private set
var hasRequested: Boolean = false companion object {
private set /**
* Id of the restartable which sends a filtered and ordered list of chapters to the view.
*/
private const val GET_CHAPTERS = 1
private val DB_CHAPTERS = 1 /**
private val FETCH_CHAPTERS = 2 * Id of the restartable which requests an updated list of chapters to the source.
private val CHAPTER_STATUS_CHANGES = 3 */
private const val FETCH_CHAPTERS = 2
/**
* Id of the restartable which listens for download status changes.
*/
private const val CHAPTER_STATUS_CHANGES = 3
}
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
chaptersSubject = PublishSubject.create() startableLatestCache(GET_CHAPTERS,
// On each subject emission, apply filters and sort then update the view.
startableLatestCache(DB_CHAPTERS, { chaptersSubject
{ getDbChaptersObs() }, .flatMap { applyChapterFilters(it) }
.observeOn(AndroidSchedulers.mainThread()) },
{ view, chapters -> view.onNextChapters(chapters) }) { view, chapters -> view.onNextChapters(chapters) })
startableFirst(FETCH_CHAPTERS, startableFirst(FETCH_CHAPTERS,
{ getOnlineChaptersObs() }, { getRemoteChaptersObservable() },
{ view, result -> view.onFetchChaptersDone() }, { view, result -> view.onFetchChaptersDone() },
{ view, error -> view.onFetchChaptersError(error) }) { view, error -> view.onFetchChaptersError(error) })
startableLatestCache(CHAPTER_STATUS_CHANGES, startableLatestCache(CHAPTER_STATUS_CHANGES,
{ getChapterStatusObs() }, { getChapterStatusObservable() },
{ view, download -> view.onChapterStatusChange(download) }, { view, download -> view.onChapterStatusChange(download) },
{ view, error -> Timber.e(error.cause, error.message) }) { view, error -> Timber.e(error.cause, error.message) })
// Find the active manga from the shared data or return.
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
Observable.just(manga) Observable.just(manga)
.subscribeLatestCache({ view, manga -> view.onNextManga(manga) }) .subscribeLatestCache({ view, manga -> view.onNextManga(manga) })
// Find the source for this manga.
source = sourceManager.get(manga.source)!! source = sourceManager.get(manga.source)!!
start(DB_CHAPTERS)
// Prepare the publish subject.
start(GET_CHAPTERS)
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
// changes, and sends the list of chapters to the publish subject.
add(db.getChapters(manga).asRxObservable() add(db.getChapters(manga).asRxObservable()
.map { chapters ->
// Convert every chapter to a model.
chapters.map { it.toModel() }
}
.doOnNext { chapters -> .doOnNext { chapters ->
// Store the last emission
this.chapters = chapters this.chapters = chapters
SharedData.get(ChapterCountEvent::class.java)?.emit(chapters.size)
for (chapter in chapters) { // Listen for download status changes
setChapterStatus(chapter)
}
start(CHAPTER_STATUS_CHANGES) start(CHAPTER_STATUS_CHANGES)
// Emit the number of chapters to the info tab.
SharedData.get(ChapterCountEvent::class.java)?.emit(chapters.size)
} }
.subscribe { chaptersSubject.onNext(it) }) .subscribe { chaptersSubject.onNext(it) })
} }
/**
* Converts a chapter from the database to an extended model, allowing to store new fields.
*/
private fun Chapter.toModel(): ChapterModel {
// Create the model object.
val model = ChapterModel(this)
// Find an active download for this chapter.
val download = downloadManager.queue.find { it.chapter.id == id }
if (download != null) {
// If there's an active download, assign it.
model.download = download
} else {
// Otherwise ask the manager if the chapter is downloaded and assign it to the status.
model.status = if (downloadManager.isChapterDownloaded(source, manga, this))
Download.DOWNLOADED
else
Download.NOT_DOWNLOADED
}
return model
}
/**
* Requests an updated list of chapters from the source.
*/
fun fetchChaptersFromSource() { fun fetchChaptersFromSource() {
hasRequested = true hasRequested = true
start(FETCH_CHAPTERS) start(FETCH_CHAPTERS)
} }
/**
* Updates the UI after applying the filters.
*/
private fun refreshChapters() { private fun refreshChapters() {
chaptersSubject.onNext(chapters) chaptersSubject.onNext(chapters)
} }
fun getOnlineChaptersObs(): Observable<Pair<Int, Int>> { /**
return source.fetchChapterList(manga) * Returns an observable that updates the chapter list with the latest from the source.
.subscribeOn(Schedulers.io()) */
.map { syncChaptersWithSource(db, it, manga, source) } fun getRemoteChaptersObservable() = source.fetchChapterList(manga)
.observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io())
} .map { syncChaptersWithSource(db, it, manga, source) }
.observeOn(AndroidSchedulers.mainThread())
fun getDbChaptersObs(): Observable<List<Chapter>> { /**
return chaptersSubject * Returns an observable that listens to download queue status changes.
.flatMap { applyChapterFilters(it) } */
.observeOn(AndroidSchedulers.mainThread()) fun getChapterStatusObservable() = downloadManager.queue.getStatusObservable()
} .observeOn(AndroidSchedulers.mainThread())
.filter { download -> download.manga.id == manga.id }
.doOnNext { onDownloadStatusChange(it) }
fun getChapterStatusObs(): Observable<Download> { /**
return downloadManager.queue.getStatusObservable() * Applies the view filters to the list of chapters obtained from the database.
.observeOn(AndroidSchedulers.mainThread()) *
.filter { download -> download.manga.id == manga.id } * @param chapters the list of chapters from the database
.doOnNext { updateChapterStatus(it) } * @return an observable of the list of chapters filtered and sorted.
} */
private fun applyChapterFilters(chapters: List<ChapterModel>): Observable<List<ChapterModel>> {
private fun applyChapterFilters(chapters: List<Chapter>): Observable<List<Chapter>> {
var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
if (onlyUnread()) { if (onlyUnread()) {
observable = observable.filter { chapter -> !chapter.read } observable = observable.filter { !it.read }
} }
if (onlyDownloaded()) { if (onlyDownloaded()) {
observable = observable.filter { chapter -> chapter.status == Download.DOWNLOADED } observable = observable.filter { it.isDownloaded }
} }
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
Manga.SORTING_SOURCE -> when (sortDescending()) { Manga.SORTING_SOURCE -> when (sortDescending()) {
@ -137,37 +226,40 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
return observable.toSortedList(sortFunction) return observable.toSortedList(sortFunction)
} }
private fun setChapterStatus(chapter: Chapter) { /**
for (download in downloadManager.queue) { * Called when a download for the active manga changes status.
if (chapter.id == download.chapter.id) { *
chapter.status = download.status * @param download the download whose status changed.
return */
fun onDownloadStatusChange(download: Download) {
// Assign the download to the model object.
if (download.status == Download.QUEUE) {
chapters.find { it.id == download.chapter.id }?.let {
if (it.download == null) {
it.download = download
}
} }
} }
if (downloadManager.isChapterDownloaded(source, manga, chapter)) { // Force UI update if downloaded filter active and download finished.
chapter.status = Download.DOWNLOADED
} else {
chapter.status = Download.NOT_DOWNLOADED
}
}
fun updateChapterStatus(download: Download) {
for (chapter in chapters) {
if (download.chapter.id == chapter.id) {
chapter.status = download.status
break
}
}
if (onlyDownloaded() && download.status == Download.DOWNLOADED) if (onlyDownloaded() && download.status == Download.DOWNLOADED)
refreshChapters() refreshChapters()
} }
fun getNextUnreadChapter(): Chapter? { /**
* Returns the next unread chapter or null if everything is read.
*/
fun getNextUnreadChapter(): ChapterModel? {
return chapters.sortedByDescending { it.source_order }.find { !it.read } return chapters.sortedByDescending { it.source_order }.find { !it.read }
} }
fun markChaptersRead(selectedChapters: List<Chapter>, read: Boolean) { /**
* Mark the selected chapter list as read/unread.
*
* @param selectedChapters the list of selected chapters.
* @param read whether to mark chapters as read or unread.
*/
fun markChaptersRead(selectedChapters: List<ChapterModel>, read: Boolean) {
Observable.from(selectedChapters) Observable.from(selectedChapters)
.doOnNext { chapter -> .doOnNext { chapter ->
chapter.read = read chapter.read = read
@ -181,21 +273,36 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
.subscribe() .subscribe()
} }
fun markPreviousChaptersAsRead(selected: Chapter) { /**
* Mark the previous chapters to the selected one as read.
*
* @param chapter the selected chapter.
*/
fun markPreviousChaptersAsRead(chapter: ChapterModel) {
Observable.from(chapters) Observable.from(chapters)
.filter { it.isRecognizedNumber && it.chapter_number < selected.chapter_number } .filter { it.isRecognizedNumber && it.chapter_number < chapter.chapter_number }
.doOnNext { it.read = true } .doOnNext { it.read = true }
.toList() .toList()
.flatMap { db.updateChaptersProgress(it).asRxObservable() } .flatMap { db.updateChaptersProgress(it).asRxObservable() }
.subscribe() .subscribe()
} }
fun downloadChapters(chapters: List<Chapter>) { /**
* Downloads the given list of chapters with the manager.
*
* @param chapters the list of chapters to download.
*/
fun downloadChapters(chapters: List<ChapterModel>) {
DownloadService.start(context) DownloadService.start(context)
downloadManager.downloadChapters(manga, chapters) downloadManager.downloadChapters(manga, chapters)
} }
fun deleteChapters(chapters: List<Chapter>) { /**
* Deletes the given list of chapter.
*
* @param chapters the list of chapters to delete.
*/
fun deleteChapters(chapters: List<ChapterModel>) {
val wasRunning = downloadManager.isRunning val wasRunning = downloadManager.isRunning
if (wasRunning) { if (wasRunning) {
DownloadService.stop(context) DownloadService.stop(context)
@ -216,49 +323,97 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
}) })
} }
private fun deleteChapter(chapter: Chapter) { /**
* Deletes a chapter from disk. This method is called in a background thread.
*
* @param chapter the chapter to delete.
*/
private fun deleteChapter(chapter: ChapterModel) {
downloadManager.queue.del(chapter) downloadManager.queue.del(chapter)
downloadManager.deleteChapter(source, manga, chapter) downloadManager.deleteChapter(source, manga, chapter)
chapter.status = Download.NOT_DOWNLOADED chapter.status = Download.NOT_DOWNLOADED
chapter.download = null
} }
/**
* Reverses the sorting and requests an UI update.
*/
fun revertSortOrder() { fun revertSortOrder() {
manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
db.updateFlags(manga).executeAsBlocking() db.updateFlags(manga).executeAsBlocking()
refreshChapters() refreshChapters()
} }
/**
* Sets the read filter and requests an UI update.
*
* @param onlyUnread whether to display only unread chapters or all chapters.
*/
fun setReadFilter(onlyUnread: Boolean) { fun setReadFilter(onlyUnread: Boolean) {
manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
db.updateFlags(manga).executeAsBlocking() db.updateFlags(manga).executeAsBlocking()
refreshChapters() refreshChapters()
} }
/**
* Sets the download filter and requests an UI update.
*
* @param onlyDownloaded whether to display only downloaded chapters or all chapters.
*/
fun setDownloadedFilter(onlyDownloaded: Boolean) { fun setDownloadedFilter(onlyDownloaded: Boolean) {
manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
db.updateFlags(manga).executeAsBlocking() db.updateFlags(manga).executeAsBlocking()
refreshChapters() refreshChapters()
} }
/**
* Removes all filters and requests an UI update.
*/
fun removeFilters() {
manga.readFilter = Manga.SHOW_ALL
manga.downloadedFilter = Manga.SHOW_ALL
db.updateFlags(manga).executeAsBlocking()
refreshChapters()
}
/**
* Sets the active display mode.
*
* @param mode the mode to set.
*/
fun setDisplayMode(mode: Int) { fun setDisplayMode(mode: Int) {
manga.displayMode = mode manga.displayMode = mode
db.updateFlags(manga).executeAsBlocking() db.updateFlags(manga).executeAsBlocking()
} }
fun setSorting(mode: Int) { /**
manga.sorting = mode * Sets the sorting method and requests an UI update.
*
* @param sort the sorting mode.
*/
fun setSorting(sort: Int) {
manga.sorting = sort
db.updateFlags(manga).executeAsBlocking() db.updateFlags(manga).executeAsBlocking()
refreshChapters() refreshChapters()
} }
/**
* Whether the display only downloaded filter is enabled.
*/
fun onlyDownloaded(): Boolean { fun onlyDownloaded(): Boolean {
return manga.downloadedFilter == Manga.SHOW_DOWNLOADED return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
} }
/**
* Whether the display only unread filter is enabled.
*/
fun onlyUnread(): Boolean { fun onlyUnread(): Boolean {
return manga.readFilter == Manga.SHOW_UNREAD return manga.readFilter == Manga.SHOW_UNREAD
} }
/**
* Whether the sorting method is descending or ascending.
*/
fun sortDescending(): Boolean { fun sortDescending(): Boolean {
return manga.sortDescending() return manga.sortDescending()
} }

View file

@ -104,7 +104,12 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
manga_genres.text = manga.genre manga_genres.text = manga.genre
// Update status TextView. // Update status TextView.
manga_status.text = manga.getStatus(activity) manga_status.setText(when (manga.status) {
Manga.ONGOING -> R.string.ongoing
Manga.COMPLETED -> R.string.completed
Manga.LICENSED -> R.string.licensed
else -> R.string.unknown
})
// Update description TextView. // Update description TextView.
manga_summary.text = manga.description manga_summary.text = manga.description

View file

@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.util.SharedData
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
/** /**
* Presenter of MangaInfoFragment. * Presenter of MangaInfoFragment.
@ -36,17 +36,17 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
/** /**
* Used to connect to database. * Used to connect to database.
*/ */
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
/** /**
* Used to connect to different manga sources. * Used to connect to different manga sources.
*/ */
@Inject lateinit var sourceManager: SourceManager val sourceManager: SourceManager by injectLazy()
/** /**
* Used to connect to cache. * Used to connect to cache.
*/ */
@Inject lateinit var coverCache: CoverCache val coverCache: CoverCache by injectLazy()
/** /**
* The id of the restartable. * The id of the restartable.

View file

@ -15,12 +15,12 @@ import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() { class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() {
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
@Inject lateinit var syncManager: MangaSyncManager val syncManager: MangaSyncManager by injectLazy()
val myAnimeList by lazy { syncManager.myAnimeList } val myAnimeList by lazy { syncManager.myAnimeList }
@ -124,7 +124,7 @@ class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() {
fun registerManga(sync: MangaSync?) { fun registerManga(sync: MangaSync?) {
if (sync != null) { if (sync != null) {
sync.manga_id = manga.id sync.manga_id = manga.id!!
add(myAnimeList.bind(sync) add(myAnimeList.bind(sync)
.flatMap { db.insertMangaSync(sync).asRxObservable() } .flatMap { db.insertMangaSync(sync).asRxObservable() }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())

View file

@ -0,0 +1,138 @@
package eu.kanade.tachiyomi.ui.reader
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.plusAssign
import rx.Observable
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
import timber.log.Timber
import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.atomic.AtomicInteger
class ChapterLoader(
private val downloadManager: DownloadManager,
private val manga: Manga,
private val source: Source
) {
private val queue = PriorityBlockingQueue<PriorityPage>()
private val subscriptions = CompositeSubscription()
fun init() {
prepareOnlineReading()
}
fun restart() {
cleanup()
init()
}
fun cleanup() {
subscriptions.clear()
queue.clear()
}
private fun prepareOnlineReading() {
subscriptions += Observable.defer { Observable.just(queue.take().page) }
.filter { it.status == Page.QUEUE }
.concatMap { source.fetchImage(it) }
.repeat()
.subscribeOn(Schedulers.io())
.subscribe({
}, {
if (it !is InterruptedException) {
Timber.e(it, it.message)
}
})
}
fun loadChapter(chapter: ReaderChapter) = Observable.just(chapter)
.flatMap {
if (chapter.pages == null)
retrievePageList(chapter)
else
Observable.just(chapter.pages!!)
}
.doOnNext { pages ->
// Now that the number of pages is known, fix the requested page if the last one
// was requested.
if (chapter.requestedPage == -1) {
chapter.requestedPage = pages.lastIndex
}
loadPages(chapter)
}
.map { chapter }
private fun retrievePageList(chapter: ReaderChapter) = Observable.just(chapter)
.flatMap {
// Check if the chapter is downloaded.
chapter.isDownloaded = downloadManager.isChapterDownloaded(source, manga, chapter)
// Fetch the page list from disk.
if (chapter.isDownloaded)
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!)
// Fetch the page list from cache or fallback to network
else
source.fetchPageList(chapter)
}
.doOnNext { pages ->
chapter.pages = pages
pages.forEach { it.chapter = chapter }
}
private fun loadPages(chapter: ReaderChapter) {
if (chapter.isDownloaded) {
loadDownloadedPages(chapter)
} else {
loadOnlinePages(chapter)
}
}
private fun loadDownloadedPages(chapter: ReaderChapter) {
val chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter)
subscriptions += Observable.from(chapter.pages!!)
.flatMap { downloadManager.getDownloadedImage(it, chapterDir) }
.subscribeOn(Schedulers.io())
.subscribe()
}
private fun loadOnlinePages(chapter: ReaderChapter) {
chapter.pages?.let { pages ->
val startPage = chapter.requestedPage
val pagesToLoad = if (startPage == 0)
pages
else
pages.drop(startPage)
pagesToLoad.forEach { queue.offer(PriorityPage(it, 0)) }
}
}
fun loadPriorizedPage(page: Page) {
queue.offer(PriorityPage(page, 1))
}
fun retryPage(page: Page) {
queue.offer(PriorityPage(page, 2))
}
private data class PriorityPage(val page: Page, val priority: Int): Comparable<PriorityPage> {
companion object {
private val idGenerator = AtomicInteger()
}
private val identifier = idGenerator.incrementAndGet()
override fun compareTo(other: PriorityPage): Int {
val p = other.priority.compareTo(priority)
return if (p != 0) p else identifier.compareTo(other.identifier)
}
}
}

View file

@ -20,7 +20,6 @@ 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.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
@ -116,16 +115,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
setSystemUiVisibility() setSystemUiVisibility()
} }
override fun onPause() {
viewer?.let {
val activePage = it.getActivePage()
if (activePage != null) {
presenter.currentPage = activePage
}
}
super.onPause()
}
override fun onDestroy() { override fun onDestroy() {
subscriptions.unsubscribe() subscriptions.unsubscribe()
popupMenu?.dismiss() popupMenu?.dismiss()
@ -230,6 +219,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
// Ignore // Ignore
} }
/**
* Called from the presenter at startup, allowing to prepare the selected reader.
*/
fun onMangaOpen(manga: Manga) { fun onMangaOpen(manga: Manga) {
if (viewer == null) { if (viewer == null) {
viewer = getOrCreateViewer(manga) viewer = getOrCreateViewer(manga)
@ -243,22 +235,23 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
please_wait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long)) please_wait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long))
} }
fun onChapterReady(manga: Manga, chapter: Chapter, currentPage: Page?) { fun onChapterReady(chapter: ReaderChapter) {
please_wait.visibility = View.GONE please_wait.visibility = View.GONE
val activePage = currentPage ?: chapter.pages.last() val pages = chapter.pages ?: run { onChapterError(Exception("Null pages")); return }
val activePage = pages.getOrElse(chapter.requestedPage) { pages.first() }
viewer?.onPageListReady(chapter, activePage) viewer?.onPageListReady(chapter, activePage)
setActiveChapter(chapter, activePage.pageNumber) setActiveChapter(chapter, activePage.pageNumber)
} }
fun onEnterChapter(chapter: Chapter, currentPage: Int) { fun onEnterChapter(chapter: ReaderChapter, currentPage: Int) {
val activePage = if (currentPage == -1) chapter.pages.lastIndex else currentPage val activePage = if (currentPage == -1) chapter.pages!!.lastIndex else currentPage
presenter.setActiveChapter(chapter) presenter.setActiveChapter(chapter)
setActiveChapter(chapter, activePage) setActiveChapter(chapter, activePage)
} }
fun setActiveChapter(chapter: Chapter, currentPage: Int) { fun setActiveChapter(chapter: ReaderChapter, currentPage: Int) {
val numPages = chapter.pages.size val numPages = chapter.pages!!.size
if (page_seekbar.rotation != 180f) { if (page_seekbar.rotation != 180f) {
right_page_text.text = "$numPages" right_page_text.text = "$numPages"
left_page_text.text = "${currentPage + 1}" left_page_text.text = "${currentPage + 1}"
@ -275,7 +268,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
chapter.name) chapter.name)
} }
fun onAppendChapter(chapter: Chapter) { fun onAppendChapter(chapter: ReaderChapter) {
viewer?.onPageListAppendReady(chapter) viewer?.onPageListAppendReady(chapter)
} }
@ -324,7 +317,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
viewer?.let { viewer?.let {
val activePage = it.getActivePage() val activePage = it.getActivePage()
if (activePage != null) { if (activePage != null) {
val requestedPage = activePage.chapter.pages[pageIndex] val requestedPage = activePage.chapter.pages!![pageIndex]
it.setActivePage(requestedPage) it.setActivePage(requestedPage)
} }

View file

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.ui.reader
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.source.model.Page
class ReaderChapter(c: Chapter) : Chapter by c {
@Transient var pages: List<Page>? = null
var isDownloaded: Boolean = false
var requestedPage: Int = 0
}

View file

@ -8,66 +8,127 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.SharedData import eu.kanade.tachiyomi.util.SharedData
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subjects.PublishSubject import uy.kohesive.injekt.injectLazy
import timber.log.Timber
import java.io.File import java.io.File
import java.util.* import java.util.*
import javax.inject.Inject
/**
* Presenter of [ReaderActivity].
*/
class ReaderPresenter : BasePresenter<ReaderActivity>() { class ReaderPresenter : BasePresenter<ReaderActivity>() {
@Inject lateinit var prefs: PreferencesHelper /**
@Inject lateinit var db: DatabaseHelper * Preferences.
@Inject lateinit var downloadManager: DownloadManager */
@Inject lateinit var syncManager: MangaSyncManager val prefs: PreferencesHelper by injectLazy()
@Inject lateinit var sourceManager: SourceManager
@Inject lateinit var chapterCache: ChapterCache
/**
* Database.
*/
val db: DatabaseHelper by injectLazy()
/**
* Download manager.
*/
val downloadManager: DownloadManager by injectLazy()
/**
* Sync manager.
*/
val syncManager: MangaSyncManager by injectLazy()
/**
* Source manager.
*/
val sourceManager: SourceManager by injectLazy()
/**
* Chapter cache.
*/
val chapterCache: ChapterCache by injectLazy()
/**
* Manga being read.
*/
lateinit var manga: Manga lateinit var manga: Manga
private set private set
lateinit var chapter: Chapter /**
* Active chapter.
*/
lateinit var chapter: ReaderChapter
private set private set
lateinit var source: Source /**
private set * Previous chapter of the active.
*/
private var prevChapter: ReaderChapter? = null
var requestedPage: Int = 0 /**
var currentPage: Page? = null * Next chapter of the active.
private var nextChapter: Chapter? = null */
private var previousChapter: Chapter? = null private var nextChapter: ReaderChapter? = null
/**
* Source of the manga.
*/
private val source by lazy { sourceManager.get(manga.source)!! }
/**
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
* time in a background thread to avoid blocking the UI.
*/
private val chapterList by lazy {
val dbChapters = db.getChapters(manga).executeAsBlocking().map { it.toModel() }
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
Manga.SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
Manga.SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
else -> throw NotImplementedError("Unknown sorting method")
}
dbChapters.sortedWith(Comparator<Chapter> { c1, c2 -> sortFunction(c1, c2) })
}
/**
* List of manga services linked to the active manga, or null if auto syncing is not enabled.
*/
private var mangaSyncList: List<MangaSync>? = null private var mangaSyncList: List<MangaSync>? = null
private val retryPageSubject by lazy { PublishSubject.create<Page>() } /**
private val pageInitializerSubject by lazy { PublishSubject.create<Chapter>() } * Chapter loader whose job is to obtain the chapter list and initialize every page.
*/
val isSeamlessMode by lazy { prefs.seamlessMode() } private val loader by lazy { ChapterLoader(downloadManager, manga, source) }
/**
* Subscription for appending a chapter to the reader (seamless mode).
*/
private var appenderSubscription: Subscription? = null private var appenderSubscription: Subscription? = null
private val PREPARE_READER = 1 /**
private val GET_PAGE_LIST = 2 * Subscription for retrieving the adjacent chapters to the current one.
private val GET_ADJACENT_CHAPTERS = 3 */
private val GET_MANGA_SYNC = 4 private var adjacentChaptersSubscription: Subscription? = null
private val PRELOAD_NEXT_CHAPTER = 5
private val MANGA_KEY = "manga_key" companion object {
private val CHAPTER_KEY = "chapter_key" /**
private val PAGE_KEY = "page_key" * Id of the restartable that loads the active chapter.
*/
private const val LOAD_ACTIVE_CHAPTER = 1
}
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@ -75,306 +136,287 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
if (savedState == null) { if (savedState == null) {
val event = SharedData.get(ReaderEvent::class.java) ?: return val event = SharedData.get(ReaderEvent::class.java) ?: return
manga = event.manga manga = event.manga
chapter = event.chapter chapter = event.chapter.toModel()
} else { } else {
manga = savedState.getSerializable(MANGA_KEY) as Manga manga = savedState.getSerializable(ReaderPresenter::manga.name) as Manga
chapter = savedState.getSerializable(CHAPTER_KEY) as Chapter chapter = savedState.getSerializable(ReaderPresenter::chapter.name) as ReaderChapter
requestedPage = savedState.getInt(PAGE_KEY)
} }
source = sourceManager.get(manga.source)!! // Send the active manga to the view to initialize the reader.
Observable.just(manga)
.subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) })
initializeSubjects() // Retrieve the sync list if auto syncing is enabled.
if (prefs.autoUpdateMangaSync()) {
add(db.getMangasSync(manga).asRxSingle()
.subscribe({ mangaSyncList = it }))
}
restartableLatestCache(PREPARE_READER, restartableLatestCache(LOAD_ACTIVE_CHAPTER,
{ Observable.just(manga) }, { loadChapterObservable(chapter) },
{ view, manga -> view.onMangaOpen(manga) }) { view, chapter -> view.onChapterReady(this.chapter) },
startableLatestCache(GET_ADJACENT_CHAPTERS,
{ getAdjacentChaptersObservable() },
{ view, pair -> view.onAdjacentChapters(pair.first, pair.second) })
startable(PRELOAD_NEXT_CHAPTER,
{ getPreloadNextChapterObservable() },
{ },
{ error -> Timber.e("Error preloading chapter") })
restartable(GET_MANGA_SYNC,
{ getMangaSyncObservable().subscribe() })
restartableLatestCache(GET_PAGE_LIST,
{ getPageListObservable(chapter) },
{ view, chapter -> view.onChapterReady(manga, this.chapter, currentPage) },
{ view, error -> view.onChapterError(error) }) { view, error -> view.onChapterError(error) })
if (savedState == null) { if (savedState == null) {
start(PREPARE_READER)
loadChapter(chapter) loadChapter(chapter)
if (prefs.autoUpdateMangaSync()) {
start(GET_MANGA_SYNC)
}
} }
} }
override fun onSave(state: Bundle) { override fun onSave(state: Bundle) {
chapter.requestedPage = chapter.last_page_read
onChapterLeft() onChapterLeft()
state.putSerializable(MANGA_KEY, manga) state.putSerializable(ReaderPresenter::manga.name, manga)
state.putSerializable(CHAPTER_KEY, chapter) state.putSerializable(ReaderPresenter::chapter.name, chapter)
state.putSerializable(PAGE_KEY, currentPage?.pageNumber ?: 0)
super.onSave(state) super.onSave(state)
} }
private fun initializeSubjects() { override fun onDestroy() {
// Listen for pages initialization events loader.cleanup()
add(pageInitializerSubject.observeOn(Schedulers.io()) super.onDestroy()
.concatMap { ch ->
val observable: Observable<Page>
if (ch.isDownloaded) {
val chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, ch)
observable = Observable.from(ch.pages)
.flatMap { downloadManager.getDownloadedImage(it, chapterDir) }
} else {
observable = source.let { source ->
if (source is OnlineSource) {
source.fetchAllImageUrlsFromPageList(ch.pages)
.flatMap({ source.getCachedImage(it) }, 2)
.doOnCompleted { source.savePageList(ch, ch.pages) }
} else {
Observable.from(ch.pages)
.flatMap { source.fetchImage(it) }
}
}
}
observable.doOnCompleted {
if (!isSeamlessMode && chapter === ch) {
preloadNextChapter()
}
}
}.subscribe())
// Listen por retry events
add(retryPageSubject.observeOn(Schedulers.io())
.flatMap { source.fetchImage(it) }
.subscribe())
} }
// Returns the page list of a chapter /**
private fun getPageListObservable(chapter: Chapter): Observable<Chapter> { * Converts a chapter to a [ReaderChapter] if needed.
val observable: Observable<List<Page>> = if (chapter.isDownloaded) */
// Fetch the page list from disk private fun Chapter.toModel(): ReaderChapter {
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!) if (this is ReaderChapter) return this
else return ReaderChapter(this)
// Fetch the page list from cache or fallback to network
source.fetchPageList(chapter)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
return observable.map { pages ->
for (page in pages) {
page.chapter = chapter
}
chapter.pages = pages
if (requestedPage >= -1 || currentPage == null) {
if (requestedPage == -1) {
currentPage = pages[pages.size - 1]
} else {
currentPage = pages[requestedPage]
}
}
requestedPage = -2
pageInitializerSubject.onNext(chapter)
chapter
}
} }
private fun getAdjacentChaptersObservable(): Observable<Pair<Chapter, Chapter>> { /**
val strategy = getAdjacentChaptersStrategy() * Returns an observable that loads the given chapter, discarding any previous work.
return Observable.zip(strategy.first, strategy.second) { prev, next -> Pair(prev, next) } *
.doOnNext { pair -> * @param chapter the now active chapter.
previousChapter = pair.first */
nextChapter = pair.second private fun loadChapterObservable(chapter: ReaderChapter): Observable<ReaderChapter> {
} loader.restart()
.observeOn(AndroidSchedulers.mainThread()) return loader.loadChapter(chapter)
}
private fun getAdjacentChaptersStrategy() = when (manga.sorting) {
Manga.SORTING_NUMBER -> Pair(
db.getPreviousChapter(chapter).asRxObservable().take(1),
db.getNextChapter(chapter).asRxObservable().take(1))
Manga.SORTING_SOURCE -> Pair(
db.getPreviousChapterBySource(chapter).asRxObservable().take(1),
db.getNextChapterBySource(chapter).asRxObservable().take(1))
else -> throw AssertionError("Unknown sorting method")
}
// Preload the first pages of the next chapter. Only for non seamless mode
private fun getPreloadNextChapterObservable(): Observable<Page> {
val nextChapter = nextChapter ?: return Observable.error(Exception("No next chapter"))
return source.fetchPageList(nextChapter)
.flatMap { pages ->
nextChapter.pages = pages
val pagesToPreload = Math.min(pages.size, 5)
Observable.from(pages).take(pagesToPreload)
}
// Preload up to 5 images
.concatMap { source.fetchImage(it) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnCompleted { stopPreloadingNextChapter() }
} }
private fun getMangaSyncObservable(): Observable<List<MangaSync>> { /**
return db.getMangasSync(manga).asRxObservable() * Obtains the adjacent chapters of the given one in a background thread, and notifies the view
.take(1) * when they are known.
.doOnNext { mangaSyncList = it } *
* @param chapter the current active chapter.
*/
private fun getAdjacentChapters(chapter: ReaderChapter) {
// Keep only one subscription
adjacentChaptersSubscription?.let { remove(it) }
adjacentChaptersSubscription = Observable
.fromCallable { getAdjacentChaptersStrategy(chapter) }
.doOnNext { pair ->
prevChapter = pair.first
nextChapter = pair.second
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache({ view, pair ->
view.onAdjacentChapters(pair.first, pair.second)
})
} }
// Loads the given chapter /**
private fun loadChapter(chapter: Chapter, requestedPage: Int = 0) { * Returns the previous and next chapters of the given one in a [Pair] according to the sorting
if (isSeamlessMode) { * strategy set for the manga.
if (appenderSubscription != null) *
remove(appenderSubscription) * @param chapter the current active chapter.
} else { */
stopPreloadingNextChapter() private fun getAdjacentChaptersStrategy(chapter: ReaderChapter) = when (manga.sorting) {
Manga.SORTING_SOURCE -> {
val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id }
val nextChapter = chapterList.getOrNull(currChapterIndex + 1)
val prevChapter = chapterList.getOrNull(currChapterIndex - 1)
Pair(prevChapter, nextChapter)
} }
Manga.SORTING_NUMBER -> {
val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id }
val chapterNumber = chapter.chapter_number
var prevChapter: ReaderChapter? = null
for (i in (currChapterIndex - 1) downTo 0) {
val c = chapterList[i]
if (c.chapter_number < chapterNumber && c.chapter_number >= chapterNumber - 1) {
prevChapter = c
break
}
}
var nextChapter: ReaderChapter? = null
for (i in (currChapterIndex + 1) until chapterList.size) {
val c = chapterList[i]
if (c.chapter_number > chapterNumber && c.chapter_number <= chapterNumber + 1) {
nextChapter = c
break
}
}
Pair(prevChapter, nextChapter)
}
else -> throw NotImplementedError("Unknown sorting method")
}
/**
* Loads the given chapter and sets it as the active one. This method also accepts a requested
* page, which will be set as active when it's displayed in the view.
*
* @param chapter the chapter to load.
* @param requestedPage the requested page from the view.
*/
private fun loadChapter(chapter: ReaderChapter, requestedPage: Int = 0) {
// Cleanup any append.
appenderSubscription?.let { remove(it) }
this.chapter = chapter this.chapter = chapter
chapter.status = if (isChapterDownloaded(chapter)) Download.DOWNLOADED else Download.NOT_DOWNLOADED
// If the chapter is partially read, set the starting page to the last the user read // If the chapter is partially read, set the starting page to the last the user read
if (!chapter.read && chapter.last_page_read != 0) // otherwise use the requested page.
this.requestedPage = chapter.last_page_read chapter.requestedPage = if (!chapter.read) chapter.last_page_read else requestedPage
else
this.requestedPage = requestedPage
// Reset next and previous chapter. They have to be fetched again // Reset next and previous chapter. They have to be fetched again
nextChapter = null nextChapter = null
previousChapter = null prevChapter = null
start(GET_PAGE_LIST) start(LOAD_ACTIVE_CHAPTER)
start(GET_ADJACENT_CHAPTERS) getAdjacentChapters(chapter)
} }
fun setActiveChapter(chapter: Chapter) { /**
* Changes the active chapter, but doesn't load anything. Called when changing chapters from
* the reader with the seamless mode.
*
* @param chapter the chapter to set as active.
*/
fun setActiveChapter(chapter: ReaderChapter) {
onChapterLeft() onChapterLeft()
this.chapter = chapter this.chapter = chapter
nextChapter = null nextChapter = null
previousChapter = null prevChapter = null
start(GET_ADJACENT_CHAPTERS) getAdjacentChapters(chapter)
} }
/**
* Appends the next chapter to the reader, if possible.
*/
fun appendNextChapter() { fun appendNextChapter() {
if (nextChapter == null) appenderSubscription?.let { remove(it) }
return
if (appenderSubscription != null) val nextChapter = nextChapter ?: return
remove(appenderSubscription)
nextChapter?.let { appenderSubscription = loader.loadChapter(nextChapter)
if (appenderSubscription != null) .subscribeOn(Schedulers.io())
remove(appenderSubscription) .retryWhen(RetryWithDelay(1, { 3000 }))
.observeOn(AndroidSchedulers.mainThread())
it.status = if (isChapterDownloaded(it)) Download.DOWNLOADED else Download.NOT_DOWNLOADED .subscribeLatestCache({ view, chapter ->
view.onAppendChapter(chapter)
appenderSubscription = getPageListObservable(it).subscribeOn(Schedulers.io()) }, { view, error ->
.observeOn(AndroidSchedulers.mainThread()) view.onChapterAppendError()
.compose(deliverLatestCache<Chapter>()) })
.subscribe(split({ view, chapter ->
view.onAppendChapter(chapter)
}, { view, error ->
view.onChapterAppendError()
}))
add(appenderSubscription)
}
}
// Check whether the given chapter is downloaded
fun isChapterDownloaded(chapter: Chapter): Boolean {
return downloadManager.isChapterDownloaded(source, manga, chapter)
} }
/**
* Retries a page that failed to load due to network error or corruption.
*
* @param page the page that failed.
*/
fun retryPage(page: Page?) { fun retryPage(page: Page?) {
if (page != null) { if (page != null && source is OnlineSource) {
page.status = Page.QUEUE page.status = Page.QUEUE
if (page.imagePath != null) { if (page.imagePath != null) {
val file = File(page.imagePath) val file = File(page.imagePath)
chapterCache.removeFileFromCache(file.name) chapterCache.removeFileFromCache(file.name)
} }
retryPageSubject.onNext(page) loader.retryPage(page)
} }
} }
// Called before loading another chapter or leaving the reader. It allows to do operations /**
// over the chapter read like saving progress * Called before loading another chapter or leaving the reader. It allows to do operations
* over the chapter read like saving progress
*/
fun onChapterLeft() { fun onChapterLeft() {
val pages = chapter.pages ?: return val pages = chapter.pages ?: return
// Get the last page read // Reference these locally because they are needed later from another thread.
var activePageNumber = chapter.last_page_read val chapter = chapter
val prevChapter = prevChapter
// Just in case, avoid out of index exceptions Observable
if (activePageNumber >= pages.size) { .fromCallable {
activePageNumber = pages.size - 1 if (!chapter.isDownloaded) {
} source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
val activePage = pages[activePageNumber]
// Cache current page list progress for online chapters to allow a faster reopen
if (!chapter.isDownloaded) {
source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
}
// Save current progress of the chapter. Mark as read if the chapter is finished
if (activePage.isLastPage) {
chapter.read = true
// Check if remove after read is selected by user
if (prefs.removeAfterRead()) {
if (prefs.removeAfterReadPrevious() ) {
if (previousChapter != null) {
deleteChapter(previousChapter!!, manga)
} }
} else {
deleteChapter(chapter, manga) // Cache current page list progress for online chapters to allow a faster reopen
if (chapter.read) {
// Check if remove after read is selected by user
if (prefs.removeAfterRead()) {
if (prefs.removeAfterReadPrevious() ) {
if (prevChapter != null) {
deleteChapter(prevChapter, manga)
}
} else {
deleteChapter(chapter, manga)
}
}
}
db.updateChapterProgress(chapter).executeAsBlocking()
val history = History.create(chapter).apply { last_read = Date().time }
db.updateHistoryLastRead(history).executeAsBlocking()
} }
} .subscribeOn(Schedulers.io())
}
db.updateChapterProgress(chapter).asRxObservable().subscribe()
// Update last read data
db.updateHistoryLastRead(History.create(chapter)
.apply { last_read = Date().time })
.asRxObservable()
.doOnError { Timber.e(it.message) }
.subscribe() .subscribe()
} }
/**
* Called when the active page changes in the reader.
*
* @param page the active page
*/
fun onPageChanged(page: Page) {
val chapter = page.chapter
chapter.last_page_read = page.pageNumber
if (chapter.pages!!.last() === page) {
chapter.read = true
}
if (!chapter.isDownloaded && page.status == Page.QUEUE) {
loader.loadPriorizedPage(page)
}
}
/** /**
* Delete selected chapter * 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) { fun deleteChapter(chapter: ReaderChapter, manga: Manga) {
val source = sourceManager.get(manga.source)!! chapter.isDownloaded = false
chapter.pages?.forEach { it.status == Page.QUEUE }
downloadManager.deleteChapter(source, manga, chapter) downloadManager.deleteChapter(source, manga, chapter)
} }
// If the current chapter has been read, we check with this one /**
// If not, we check if the previous chapter has been read * Returns the chapter to be marked as last read in sync services or 0 if no update required.
// We know the chapter we have to check, but we don't know yet if an update is required. */
// This boolean is used to return 0 if no update is required
fun getMangaSyncChapterToUpdate(): Int { fun getMangaSyncChapterToUpdate(): Int {
if (chapter.pages == null || mangaSyncList == null || mangaSyncList!!.isEmpty()) if (chapter.pages == null || mangaSyncList == null || mangaSyncList!!.isEmpty())
return 0 return 0
var lastChapterReadLocal = 0 var lastChapterReadLocal = 0
// If the current chapter has been read, we check with this one
if (chapter.read) if (chapter.read)
lastChapterReadLocal = Math.floor(chapter.chapter_number.toDouble()).toInt() lastChapterReadLocal = Math.floor(chapter.chapter_number.toDouble()).toInt()
else if (previousChapter != null && previousChapter!!.read) // If not, we check if the previous chapter has been read
lastChapterReadLocal = Math.floor(previousChapter!!.chapter_number.toDouble()).toInt() else if (prevChapter != null && prevChapter!!.read)
lastChapterReadLocal = Math.floor(prevChapter!!.chapter_number.toDouble()).toInt()
// We know the chapter we have to check, but we don't know yet if an update is required.
// This boolean is used to return 0 if no update is required
var hasToUpdate = false var hasToUpdate = false
for (mangaSync in mangaSyncList!!) { for (mangaSync in mangaSyncList!!) {
@ -387,6 +429,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
return if (hasToUpdate) lastChapterReadLocal else 0 return if (hasToUpdate) lastChapterReadLocal else 0
} }
/**
* Starts the service that updates the last chapter read in sync services
*/
fun updateMangaSyncLastChapterRead() { fun updateMangaSyncLastChapterRead() {
for (mangaSync in mangaSyncList ?: emptyList()) { for (mangaSync in mangaSyncList ?: emptyList()) {
val service = syncManager.getService(mangaSync.sync_id) ?: continue val service = syncManager.getService(mangaSync.sync_id) ?: continue
@ -396,6 +441,11 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
} }
} }
/**
* Loads the next chapter.
*
* @return true if the next chapter is being loaded, false if there is no next chapter.
*/
fun loadNextChapter(): Boolean { fun loadNextChapter(): Boolean {
nextChapter?.let { nextChapter?.let {
onChapterLeft() onChapterLeft()
@ -405,44 +455,42 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
return false return false
} }
/**
* Loads the next chapter.
*
* @return true if the previous chapter is being loaded, false if there is no previous chapter.
*/
fun loadPreviousChapter(): Boolean { fun loadPreviousChapter(): Boolean {
previousChapter?.let { prevChapter?.let {
onChapterLeft() onChapterLeft()
loadChapter(it, 0) loadChapter(it, if (it.read) -1 else 0)
return true return true
} }
return false return false
} }
/**
* Returns true if there's a next chapter.
*/
fun hasNextChapter(): Boolean { fun hasNextChapter(): Boolean {
return nextChapter != null return nextChapter != null
} }
/**
* Returns true if there's a previous chapter.
*/
fun hasPreviousChapter(): Boolean { fun hasPreviousChapter(): Boolean {
return previousChapter != null return prevChapter != null
}
private fun preloadNextChapter() {
nextChapter?.let {
if (!isChapterDownloaded(it)) {
start(PRELOAD_NEXT_CHAPTER)
}
}
}
private fun stopPreloadingNextChapter() {
if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) {
stop(PRELOAD_NEXT_CHAPTER)
nextChapter?.let { chapter ->
if (chapter.pages != null) {
source.let { if (it is OnlineSource) it.savePageList(chapter, chapter.pages) }
}
}
}
} }
/**
* Updates the viewer for this manga.
*
* @param viewer the id of the viewer to set.
*/
fun updateMangaViewer(viewer: Int) { fun updateMangaViewer(viewer: Int) {
manga.viewer = viewer manga.viewer = viewer
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
} }
} }

View file

@ -1,11 +1,11 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base package eu.kanade.tachiyomi.ui.reader.viewer.base
import com.davemorrissey.labs.subscaleview.decoder.* import com.davemorrissey.labs.subscaleview.decoder.*
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
import java.util.* import java.util.*
/** /**
@ -29,7 +29,7 @@ abstract class BaseReader : BaseFragment() {
/** /**
* List of chapters added in the reader. * List of chapters added in the reader.
*/ */
private var chapters = ArrayList<Chapter>() private val chapters = ArrayList<ReaderChapter>()
/** /**
* List of pages added in the reader. It can contain pages from more than one chapter. * List of pages added in the reader. It can contain pages from more than one chapter.
@ -72,7 +72,7 @@ abstract class BaseReader : BaseFragment() {
fun updatePageNumber() { fun updatePageNumber() {
val activePage = getActivePage() val activePage = getActivePage()
if (activePage != null) { if (activePage != null) {
readerActivity.onPageChanged(activePage.pageNumber, activePage.chapter.pages.size) readerActivity.onPageChanged(activePage.pageNumber, activePage.chapter.pages!!.size)
} }
} }
@ -91,23 +91,22 @@ abstract class BaseReader : BaseFragment() {
fun onPageChanged(position: Int) { fun onPageChanged(position: Int) {
val oldPage = pages[currentPage] val oldPage = pages[currentPage]
val newPage = pages[position] val newPage = pages[position]
newPage.chapter.last_page_read = newPage.pageNumber readerActivity.presenter.onPageChanged(newPage)
if (readerActivity.presenter.isSeamlessMode) { val oldChapter = oldPage.chapter
val oldChapter = oldPage.chapter val newChapter = newPage.chapter
val newChapter = newPage.chapter
// Active chapter has changed. // Active chapter has changed.
if (oldChapter.id != newChapter.id) { if (oldChapter.id != newChapter.id) {
readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber) readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber)
}
// Request next chapter only when the conditions are met.
if (pages.size - position < 5 && chapters.last().id == newChapter.id
&& readerActivity.presenter.hasNextChapter() && !hasRequestedNextChapter) {
hasRequestedNextChapter = true
readerActivity.presenter.appendNextChapter()
}
} }
// Request next chapter only when the conditions are met.
if (pages.size - position < 5 && chapters.last().id == newChapter.id
&& readerActivity.presenter.hasNextChapter() && !hasRequestedNextChapter) {
hasRequestedNextChapter = true
readerActivity.presenter.appendNextChapter()
}
currentPage = position currentPage = position
updatePageNumber() updatePageNumber()
} }
@ -144,10 +143,10 @@ abstract class BaseReader : BaseFragment() {
* @param chapter the chapter to set. * @param chapter the chapter to set.
* @param currentPage the initial page to display. * @param currentPage the initial page to display.
*/ */
fun onPageListReady(chapter: Chapter, currentPage: Page) { fun onPageListReady(chapter: ReaderChapter, currentPage: Page) {
if (!chapters.contains(chapter)) { if (!chapters.contains(chapter)) {
// if we reset the loaded page we also need to reset the loaded chapters // if we reset the loaded page we also need to reset the loaded chapters
chapters = ArrayList<Chapter>() chapters.clear()
chapters.add(chapter) chapters.add(chapter)
pages = ArrayList(chapter.pages) pages = ArrayList(chapter.pages)
onChapterSet(chapter, currentPage) onChapterSet(chapter, currentPage)
@ -162,11 +161,11 @@ abstract class BaseReader : BaseFragment() {
* *
* @param chapter the chapter to append. * @param chapter the chapter to append.
*/ */
fun onPageListAppendReady(chapter: Chapter) { fun onPageListAppendReady(chapter: ReaderChapter) {
if (!chapters.contains(chapter)) { if (!chapters.contains(chapter)) {
hasRequestedNextChapter = false hasRequestedNextChapter = false
chapters.add(chapter) chapters.add(chapter)
pages.addAll(chapter.pages) pages.addAll(chapter.pages!!)
onChapterAppended(chapter) onChapterAppended(chapter)
} }
} }
@ -184,14 +183,14 @@ abstract class BaseReader : BaseFragment() {
* @param chapter the chapter set. * @param chapter the chapter set.
* @param currentPage the initial page to display. * @param currentPage the initial page to display.
*/ */
abstract fun onChapterSet(chapter: Chapter, currentPage: Page) abstract fun onChapterSet(chapter: ReaderChapter, currentPage: Page)
/** /**
* Called when a chapter is appended in [BaseReader]. * Called when a chapter is appended in [BaseReader].
* *
* @param chapter the chapter appended. * @param chapter the chapter appended.
*/ */
abstract fun onChapterAppended(chapter: Chapter) abstract fun onChapterAppended(chapter: ReaderChapter)
/** /**
* Moves pages forward. Implementations decide how to move (by a page, by some distance...). * Moves pages forward. Implementations decide how to move (by a page, by some distance...).

View file

@ -5,8 +5,8 @@ import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
@ -181,7 +181,7 @@ abstract class PagerReader : BaseReader() {
* @param chapter the chapter set. * @param chapter the chapter set.
* @param currentPage the initial page to display. * @param currentPage the initial page to display.
*/ */
override fun onChapterSet(chapter: Chapter, currentPage: Page) { override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) {
this.currentPage = getPageIndex(currentPage) // we might have a new page object this.currentPage = getPageIndex(currentPage) // we might have a new page object
// Make sure the view is already initialized. // Make sure the view is already initialized.
@ -195,7 +195,7 @@ abstract class PagerReader : BaseReader() {
* *
* @param chapter the chapter appended. * @param chapter the chapter appended.
*/ */
override fun onChapterAppended(chapter: Chapter) { override fun onChapterAppended(chapter: ReaderChapter) {
// Make sure the view is already initialized. // Make sure the view is already initialized.
if (view != null) { if (view != null) {
adapter.pages = pages adapter.pages = pages

View file

@ -6,8 +6,8 @@ import android.view.*
import android.view.GestureDetector.SimpleOnGestureListener import android.view.GestureDetector.SimpleOnGestureListener
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
import eu.kanade.tachiyomi.widget.PreCachingLayoutManager import eu.kanade.tachiyomi.widget.PreCachingLayoutManager
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
@ -147,7 +147,7 @@ class WebtoonReader : BaseReader() {
* @param chapter the chapter set. * @param chapter the chapter set.
* @param currentPage the initial page to display. * @param currentPage the initial page to display.
*/ */
override fun onChapterSet(chapter: Chapter, currentPage: Page) { override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) {
// Restoring current page is not supported. It's getting weird scrolling jumps // Restoring current page is not supported. It's getting weird scrolling jumps
// this.currentPage = currentPage; // this.currentPage = currentPage;
@ -162,11 +162,11 @@ class WebtoonReader : BaseReader() {
* *
* @param chapter the chapter appended. * @param chapter the chapter appended.
*/ */
override fun onChapterAppended(chapter: Chapter) { override fun onChapterAppended(chapter: ReaderChapter) {
// Make sure the view is already initialized. // Make sure the view is already initialized.
if (view != null) { if (view != null) {
val insertStart = pages.size - chapter.pages.size val insertStart = pages.size - chapter.pages!!.size
adapter.notifyItemRangeInserted(insertStart, chapter.pages.size) adapter.notifyItemRangeInserted(insertStart, chapter.pages!!.size)
} }
} }

View file

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.ui.recent_updates
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.download.model.Download
class RecentChapter(mc: MangaChapter) : Chapter by mc.chapter {
val manga = mc.manga
private var _status: Int = 0
var status: Int
get() = download?.status ?: _status
set(value) { _status = value }
var download: Download? = null
val isDownloaded: Boolean
get() = status == Download.DOWNLOADED
}

View file

@ -5,7 +5,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import java.util.* import java.util.*
@ -18,7 +17,8 @@ import java.util.*
* @constructor creates an instance of the adapter. * @constructor creates an instance of the adapter.
*/ */
class RecentChaptersAdapter(val fragment: RecentChaptersFragment) : FlexibleAdapter<RecyclerView.ViewHolder, Any>() { class RecentChaptersAdapter(val fragment: RecentChaptersFragment)
: FlexibleAdapter<RecyclerView.ViewHolder, Any>() {
/** /**
* The id of the view type * The id of the view type
*/ */
@ -45,7 +45,7 @@ class RecentChaptersAdapter(val fragment: RecentChaptersFragment) : FlexibleAdap
val item = getItem(position) val item = getItem(position)
when (holder.itemViewType) { when (holder.itemViewType) {
VIEW_TYPE_CHAPTER -> { VIEW_TYPE_CHAPTER -> {
if (item is MangaChapter) { if (item is RecentChapter) {
(holder as RecentChaptersHolder).onSetValues(item) (holder as RecentChaptersHolder).onSetValues(item)
} }
} }
@ -89,7 +89,7 @@ class RecentChaptersAdapter(val fragment: RecentChaptersFragment) : FlexibleAdap
* @param position position of item * @param position position of item
*/ */
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return if (getItem(position) is MangaChapter) VIEW_TYPE_CHAPTER else VIEW_TYPE_SECTION return if (getItem(position) is RecentChapter) VIEW_TYPE_CHAPTER else VIEW_TYPE_SECTION
} }
@ -110,8 +110,8 @@ class RecentChaptersAdapter(val fragment: RecentChaptersFragment) : FlexibleAdap
*/ */
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
val item = getItem(position) val item = getItem(position)
if (item is MangaChapter) if (item is RecentChapter)
return item.chapter.id return item.id!!
else else
return item.hashCode().toLong() return item.hashCode().toLong()
} }

View file

@ -7,14 +7,12 @@ import android.view.*
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
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.ui.base.fragment.BaseRxFragment 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.util.toast
import eu.kanade.tachiyomi.widget.DeletingChaptersDialog import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
import eu.kanade.tachiyomi.widget.DividerItemDecoration import eu.kanade.tachiyomi.widget.DividerItemDecoration
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
@ -28,7 +26,9 @@ import timber.log.Timber
* UI related actions should be called from here. * UI related actions should be called from here.
*/ */
@RequiresPresenter(RecentChaptersPresenter::class) @RequiresPresenter(RecentChaptersPresenter::class)
class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener { class RecentChaptersFragment
: BaseRxFragment<RecentChaptersPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
companion object { companion object {
/** /**
* Create new RecentChaptersFragment. * Create new RecentChaptersFragment.
@ -40,6 +40,230 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Action
} }
} }
/**
* Action mode for multiple selection.
*/
private var actionMode: ActionMode? = null
/**
* Adapter containing the recent chapters.
*/
lateinit var adapter: RecentChaptersAdapter
private set
/**
* Called when view gets created
* @param inflater layout inflater
* @param container view group
* @param savedState status of saved state
*/
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
// Inflate view
return inflater.inflate(R.layout.fragment_recent_chapters, container, false)
}
/**
* Called when view is created
* @param view created view
* @param savedInstanceState status of saved sate
*/
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Init RecyclerView and adapter
recycler.layoutManager = NpaLinearLayoutManager(activity)
recycler.addItemDecoration(DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable)))
recycler.setHasFixedSize(true)
adapter = RecentChaptersAdapter(this)
recycler.adapter = adapter
// Update toolbar text
setToolbarTitle(R.string.label_recent_updates)
}
/**
* Returns selected chapters
* @return list of selected chapters
*/
fun getSelectedChapters(): List<RecentChapter> {
return adapter.selectedItems.map { adapter.getItem(it) as? RecentChapter }.filterNotNull()
}
/**
* Called when item in list is clicked
* @param position position of clicked item
*/
override fun onListItemClick(position: Int): Boolean {
// Get item from position
val item = adapter.getItem(position)
if (item is RecentChapter) {
if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
toggleSelection(position)
return true
} else {
openChapter(item)
return false
}
}
return false
}
/**
* Called when item in list is long clicked
* @param position position of clicked item
*/
override fun onListItemLongClick(position: Int) {
if (actionMode == null)
actionMode = activity.startSupportActionMode(this)
toggleSelection(position)
}
/**
* Called to toggle selection
* @param position position of selected item
*/
private fun toggleSelection(position: Int) {
adapter.toggleSelection(position, false)
val count = adapter.selectedItemCount
if (count == 0) {
actionMode?.finish()
} else {
setContextTitle(count)
actionMode?.invalidate()
}
}
/**
* Set the context title
* @param count count of selected items
*/
private fun setContextTitle(count: Int) {
actionMode?.title = getString(R.string.label_selected, count)
}
/**
* Open chapter in reader
* @param chapter selected chapter
*/
private fun openChapter(chapter: RecentChapter) {
val intent = ReaderActivity.newIntent(activity, chapter.manga, chapter)
startActivity(intent)
}
/**
* Download selected items
* @param chapters list of selected [RecentChapter]s
*/
fun downloadChapters(chapters: List<RecentChapter>) {
destroyActionModeIfNeeded()
presenter.downloadChapters(chapters)
}
/**
* Populate adapter with chapters
* @param chapters list of [Any]
*/
fun onNextRecentChapters(chapters: List<Any>) {
(activity as MainActivity).updateEmptyView(chapters.isEmpty(),
R.string.information_no_recent, R.drawable.ic_update_black_128dp)
destroyActionModeIfNeeded()
adapter.setItems(chapters)
}
/**
* Update download status of chapter
* @param download [Download] object containing download progress.
*/
fun onChapterStatusChange(download: Download) {
getHolder(download)?.notifyStatus(download.status)
}
/**
* Returns holder belonging to chapter
* @param download [Download] object containing download progress.
*/
private fun getHolder(download: Download): RecentChaptersHolder? {
return recycler.findViewHolderForItemId(download.chapter.id!!) as? RecentChaptersHolder
}
/**
* Mark chapter as read
* @param chapters list of chapters
*/
fun markAsRead(chapters: List<RecentChapter>) {
presenter.markChapterRead(chapters, true)
if (presenter.preferences.removeAfterMarkedAsRead()) {
deleteChapters(chapters)
}
}
/**
* Delete selected chapters
* @param chapters list of [RecentChapter] objects
*/
fun deleteChapters(chapters: List<RecentChapter>) {
destroyActionModeIfNeeded()
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
presenter.deleteChapters(chapters)
}
/**
* Destory [ActionMode] if it's shown
*/
fun destroyActionModeIfNeeded() {
actionMode?.finish()
}
/**
* Mark chapter as unread
* @param chapters list of selected [RecentChapter]
*/
fun markAsUnread(chapters: List<RecentChapter>) {
presenter.markChapterRead(chapters, false)
}
/**
* Start downloading chapter
* @param chapter selected chapter with manga
*/
fun downloadChapter(chapter: RecentChapter) {
presenter.downloadChapter(chapter)
}
/**
* Start deleting chapter
* @param chapter selected chapter with manga
*/
fun deleteChapter(chapter: RecentChapter) {
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
presenter.deleteChapters(listOf(chapter))
}
/**
* Called when chapters are deleted
*/
fun onChaptersDeleted() {
dismissDeletingDialog()
adapter.notifyDataSetChanged()
}
/**
* Called when error while deleting
* @param error error message
*/
fun onChaptersDeletedError(error: Throwable) {
dismissDeletingDialog()
Timber.e(error, error.message)
}
/**
* Called to dismiss deleting dialog
*/
fun dismissDeletingDialog() {
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false return false
} }
@ -88,229 +312,4 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Action
actionMode = null actionMode = null
} }
/**
* Action mode for multiple selection.
*/
private var actionMode: ActionMode? = null
/**
* Adapter containing the recent chapters.
*/
lateinit var adapter: RecentChaptersAdapter
private set
/**
* Called when view gets created
* @param inflater layout inflater
* @param container view group
* @param savedState status of saved state
*/
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
// Inflate view
return inflater.inflate(R.layout.fragment_recent_chapters, container, false)
}
/**
* Called when view is created
* @param view created view
* @param savedInstanceState status of saved sate
*/
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
// Init RecyclerView and adapter
recycler.layoutManager = NpaLinearLayoutManager(activity)
recycler.addItemDecoration(DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable)))
recycler.setHasFixedSize(true)
adapter = RecentChaptersAdapter(this)
recycler.adapter = adapter
// Update toolbar text
setToolbarTitle(R.string.label_recent_updates)
}
/**
* Returns selected chapters
* @return list of [MangaChapter]s
*/
fun getSelectedChapters(): List<MangaChapter> {
return adapter.selectedItems.map { adapter.getItem(it) as? MangaChapter }.filterNotNull()
}
/**
* Called when item in list is clicked
* @param position position of clicked item
*/
override fun onListItemClick(position: Int): Boolean {
// Get item from position
val item = adapter.getItem(position)
if (item is MangaChapter) {
if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
toggleSelection(position)
return true
} else {
openChapter(item)
return false
}
}
return false
}
/**
* Called when item in list is long clicked
* @param position position of clicked item
*/
override fun onListItemLongClick(position: Int) {
if (actionMode == null)
actionMode = activity.startSupportActionMode(this)
toggleSelection(position)
}
/**
* Called to toggle selection
* @param position position of selected item
*/
private fun toggleSelection(position: Int) {
adapter.toggleSelection(position, false)
val count = adapter.selectedItemCount
if (count == 0) {
actionMode?.finish()
} else {
setContextTitle(count)
actionMode?.invalidate()
}
}
/**
* Set the context title
* @param count count of selected items
*/
private fun setContextTitle(count: Int) {
actionMode?.title = getString(R.string.label_selected, count)
}
/**
* Open chapter in reader
* @param mangaChapter selected [MangaChapter]
*/
private fun openChapter(mangaChapter: MangaChapter) {
val intent = ReaderActivity.newIntent(activity, mangaChapter.manga, mangaChapter.chapter)
startActivity(intent)
}
/**
* Download selected items
* @param mangaChapters list of selected [MangaChapter]s
*/
fun downloadChapters(mangaChapters: List<MangaChapter>) {
destroyActionModeIfNeeded()
presenter.downloadChapters(mangaChapters)
}
/**
* Populate adapter with chapters
* @param chapters list of [Any]
*/
fun onNextMangaChapters(chapters: List<Any>) {
(activity as MainActivity).updateEmptyView(chapters.isEmpty(),
R.string.information_no_recent, R.drawable.ic_update_black_128dp)
destroyActionModeIfNeeded()
adapter.setItems(chapters)
}
/**
* Update download status of chapter
* @param download [Download] object containing download progress.
*/
fun onChapterStatusChange(download: Download) {
getHolder(download)?.onStatusChange(download.status)
}
/**
* Returns holder belonging to chapter
* @param download [Download] object containing download progress.
*/
private fun getHolder(download: Download): RecentChaptersHolder? {
return recycler.findViewHolderForItemId(download.chapter.id) as? RecentChaptersHolder
}
/**
* Mark chapter as read
* @param mangaChapters list of [MangaChapter] objects
*/
fun markAsRead(mangaChapters: List<MangaChapter>) {
presenter.markChapterRead(mangaChapters, true)
if (presenter.preferences.removeAfterMarkedAsRead()) {
deleteChapters(mangaChapters)
}
}
/**
* Delete selected chapters
* @param mangaChapters list of [MangaChapter] objects
*/
fun deleteChapters(mangaChapters: List<MangaChapter>) {
destroyActionModeIfNeeded()
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
presenter.deleteChapters(mangaChapters)
}
/**
* Destory [ActionMode] if it's shown
*/
fun destroyActionModeIfNeeded() {
actionMode?.finish()
}
/**
* Mark chapter as unread
* @param mangaChapters list of selected [MangaChapter]
*/
fun markAsUnread(mangaChapters: List<MangaChapter>) {
presenter.markChapterRead(mangaChapters, false)
}
/**
* Start downloading chapter
* @param item selected chapter with manga
*/
fun downloadChapter(item: MangaChapter) {
presenter.downloadChapter(item)
}
/**
* Start deleting chapter
* @param item selected chapter with manga
*/
fun deleteChapter(item: MangaChapter) {
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
presenter.deleteChapter(item)
}
/**
* Called when chapters are deleted
*/
fun onChaptersDeleted() {
dismissDeletingDialog()
adapter.notifyDataSetChanged()
}
/**
* Called when error while deleting
* @param error error message
*/
fun onChaptersDeletedError(error: Throwable) {
dismissDeletingDialog()
Timber.e(error, error.message)
}
/**
* Called to dismiss deleting dialog
*/
fun dismissDeletingDialog() {
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
}
} }

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.recent_updates
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.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
@ -19,8 +18,11 @@ import kotlinx.android.synthetic.main.item_recent_chapters.view.*
* @param listener a listener to react to single tap and long tap events. * @param listener a listener to react to single tap and long tap events.
* @constructor creates a new recent chapter holder. * @constructor creates a new recent chapter holder.
*/ */
class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapter, listener: OnListItemClickListener) : class RecentChaptersHolder(
FlexibleViewHolder(view, adapter, listener) { private val view: View,
private val adapter: RecentChaptersAdapter,
listener: OnListItemClickListener)
: FlexibleViewHolder(view, adapter, listener) {
/** /**
* Color of read chapter * Color of read chapter
*/ */
@ -34,100 +36,97 @@ class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapte
/** /**
* Object containing chapter information * Object containing chapter information
*/ */
private var mangaChapter: MangaChapter? = null private var chapter: RecentChapter? = null
init { init {
// We need to post a Runnable to show the popup to make sure that the PopupMenu is // 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 // correctly positioned. The reason being that the view may change position before the
// PopupMenu is shown. // PopupMenu is shown.
itemView.chapter_menu.setOnClickListener { it.post({ showPopupMenu(it) }) } view.chapter_menu.setOnClickListener { it.post({ showPopupMenu(it) }) }
} }
/** /**
* Set values of view * Set values of view
* *
* @param item item containing chapter information * @param chapter item containing chapter information
*/ */
fun onSetValues(item: MangaChapter) { fun onSetValues(chapter: RecentChapter) {
this.mangaChapter = item this.chapter = chapter
// Set chapter title // Set chapter title
itemView.chapter_title.text = item.chapter.name view.chapter_title.text = chapter.name
// Set manga title // Set manga title
itemView.manga_title.text = item.manga.title view.manga_title.text = chapter.manga.title
// Check if chapter is read and set correct color // Check if chapter is read and set correct color
if (item.chapter.read) { if (chapter.read) {
itemView.chapter_title.setTextColor(readColor) view.chapter_title.setTextColor(readColor)
itemView.manga_title.setTextColor(readColor) view.manga_title.setTextColor(readColor)
} else { } else {
itemView.chapter_title.setTextColor(unreadColor) view.chapter_title.setTextColor(unreadColor)
itemView.manga_title.setTextColor(unreadColor) view.manga_title.setTextColor(unreadColor)
} }
// Set chapter status // Set chapter status
onStatusChange(item.chapter.status) notifyStatus(chapter.status)
} }
/** /**
* Updates chapter status in view. * Updates chapter status in view.
*
* @param status download status * @param status download status
*/ */
fun onStatusChange(status: Int) { fun notifyStatus(status: Int) = with(view.download_text) {
when (status) { when (status) {
Download.QUEUE -> itemView.download_text.setText(R.string.chapter_queued) Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> itemView.download_text.setText(R.string.chapter_downloading) Download.DOWNLOADING -> setText(R.string.chapter_downloading)
Download.DOWNLOADED -> itemView.download_text.setText(R.string.chapter_downloaded) Download.DOWNLOADED -> setText(R.string.chapter_downloaded)
Download.ERROR -> itemView.download_text.setText(R.string.chapter_error) Download.ERROR -> setText(R.string.chapter_error)
else -> itemView.download_text.text = "" else -> text = ""
} }
} }
/** /**
* Show pop up menu * Show pop up menu
*
* @param view view containing popup menu. * @param view view containing popup menu.
*/ */
private fun showPopupMenu(view: View) { private fun showPopupMenu(view: View) = chapter?.let { chapter ->
// Create a PopupMenu, giving it the clicked view for an anchor // Create a PopupMenu, giving it the clicked view for an anchor
val popup = PopupMenu(adapter.fragment.activity, view) val popup = PopupMenu(view.context, view)
// Inflate our menu resource into the PopupMenu's Menu // Inflate our menu resource into the PopupMenu's Menu
popup.menuInflater.inflate(R.menu.chapter_recent, popup.menu) popup.menuInflater.inflate(R.menu.chapter_recent, popup.menu)
mangaChapter?.let { // Hide download and show delete if the chapter is downloaded and
if (chapter.isDownloaded) {
popup.menu.findItem(R.id.action_download).isVisible = false
popup.menu.findItem(R.id.action_delete).isVisible = true
}
// Hide download and show delete if the chapter is downloaded and // Hide mark as unread when the chapter is unread
if (it.chapter.isDownloaded) { if (!chapter.read /*&& mangaChapter.chapter.last_page_read == 0*/) {
val menu = popup.menu popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false
menu.findItem(R.id.action_download).isVisible = false }
menu.findItem(R.id.action_delete).isVisible = true
}
// Hide mark as unread when the chapter is unread // Hide mark as read when the chapter is read
if (!it.chapter.read /*&& mangaChapter.chapter.last_page_read == 0*/) { if (chapter.read) {
popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false popup.menu.findItem(R.id.action_mark_as_read).isVisible = false
} }
// Hide mark as read when the chapter is read
if (it.chapter.read) {
popup.menu.findItem(R.id.action_mark_as_read).isVisible = false
}
// Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem ->
// Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem ->
with(adapter.fragment) {
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.action_download -> adapter.fragment.downloadChapter(it) R.id.action_download -> downloadChapter(chapter)
R.id.action_delete -> adapter.fragment.deleteChapter(it) R.id.action_delete -> deleteChapter(chapter)
R.id.action_mark_as_read -> adapter.fragment.markAsRead(listOf(it)) R.id.action_mark_as_read -> markAsRead(listOf(chapter))
R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(listOf(it)) R.id.action_mark_as_unread -> markAsUnread(listOf(chapter))
} }
true
} }
true
} }
// Finally show the PopupMenu // Finally show the PopupMenu

View file

@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.ui.recent_updates
import android.os.Bundle import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper 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.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.DownloadService
@ -15,34 +13,34 @@ import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.util.* import java.util.*
import javax.inject.Inject
class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() { class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Used to connect to database * Used to connect to database
*/ */
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
/** /**
* Used to get settings * Used to get settings
*/ */
@Inject lateinit var preferences: PreferencesHelper val preferences: PreferencesHelper by injectLazy()
/** /**
* Used to get information from download manager * Used to get information from download manager
*/ */
@Inject lateinit var downloadManager: DownloadManager val downloadManager: DownloadManager by injectLazy()
/** /**
* Used to get source from source id * Used to get source from source id
*/ */
@Inject lateinit var sourceManager: SourceManager val sourceManager: SourceManager by injectLazy()
/** /**
* List containing chapter and manga information * List containing chapter and manga information
*/ */
private var mangaChapters: List<MangaChapter>? = null private var chapters: List<RecentChapter>? = null
/** /**
* The id of the restartable. * The id of the restartable.
@ -60,139 +58,26 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
// Used to get recent chapters // Used to get recent chapters
restartableLatestCache(GET_RECENT_CHAPTERS, restartableLatestCache(GET_RECENT_CHAPTERS,
{ getRecentChaptersObservable() }, { getRecentChaptersObservable() },
{ fragment, chapters -> { view, chapters ->
// Update adapter to show recent manga's // Update adapter to show recent manga's
fragment.onNextMangaChapters(chapters) view.onNextRecentChapters(chapters)
// Update download status
updateChapterStatus(convertToMangaChaptersList(chapters))
} }
) )
// Used to update download status // Used to update download status
startableLatestCache(CHAPTER_STATUS_CHANGES, restartableLatestCache(CHAPTER_STATUS_CHANGES,
{ getChapterStatusObs() }, { getChapterStatusObservable() },
{ recentChaptersFragment, download -> { view, download ->
// Set chapter status // Set chapter status
recentChaptersFragment.onChapterStatusChange(download) view.onChapterStatusChange(download)
}, },
{ view, error -> Timber.e(error.cause, error.message) } { view, error -> Timber.e(error.cause, error.message) }
) )
if (savedState == null) { if (savedState == null) {
// Start fetching recent chapters // Start fetching recent chapters
start(GET_RECENT_CHAPTERS) start(GET_RECENT_CHAPTERS)
} start(CHAPTER_STATUS_CHANGES)
}
/**
* Returns observable containing chapter status.
* @return download object containing download progress.
*/
private fun getChapterStatusObs(): Observable<Download> {
return downloadManager.queue.getStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.filter { download: Download ->
if (chapterIdEquals(download.chapter.id))
true
else
false
}
.doOnNext { download1: Download -> updateChapterStatus(download1) }
}
/**
* Function to check if chapter is in recent list
* @param chaptersId id of chapter
* @return exist in recent list
*/
private fun chapterIdEquals(chaptersId: Long): Boolean {
mangaChapters!!.forEach { mangaChapter ->
if (chaptersId == mangaChapter.chapter.id) {
return true
}
}
return false
}
/**
* Returns a list only containing MangaChapter objects.
* @param input the list that will be converted.
* @return list containing MangaChapters objects.
*/
private fun convertToMangaChaptersList(input: List<Any>): List<MangaChapter> {
// Create temp list
val tempMangaChapterList = ArrayList<MangaChapter>()
// Only add MangaChapter objects
//noinspection Convert2streamapi
input.forEach { `object` ->
if (`object` is MangaChapter) {
tempMangaChapterList.add(`object`)
}
}
// Return temp list
return tempMangaChapterList
}
/**
* Update status of chapters.
* @param download download object containing progress.
*/
private fun updateChapterStatus(download: Download) {
// Loop through list
mangaChapters?.let {
for (item in it) {
if (download.chapter.id == item.chapter.id) {
// Update status.
item.chapter.status = download.status
break
}
}
}
}
/**
* Update status of chapters
* @param mangaChapters list containing recent chapters
*/
private fun updateChapterStatus(mangaChapters: List<MangaChapter>) {
// Set global list of chapters.
this.mangaChapters = mangaChapters
// Update status.
//noinspection Convert2streamapi
for (mangaChapter in mangaChapters)
setChapterStatus(mangaChapter)
// Start onChapterStatusChange restartable.
start(CHAPTER_STATUS_CHANGES)
}
/**
* Set the chapter status
* @param mangaChapter MangaChapter which status gets updated
*/
private fun setChapterStatus(mangaChapter: MangaChapter) {
// Check if chapter in queue
for (download in downloadManager.queue) {
if (mangaChapter.chapter.id == download.chapter.id) {
mangaChapter.chapter.status = download.status
return
}
}
// Get source of chapter
val source = sourceManager.get(mangaChapter.manga.source)!!
// Check if chapter is downloaded
if (downloadManager.isChapterDownloaded(source, mangaChapter.manga, mangaChapter.chapter)) {
mangaChapter.chapter.status = Download.DOWNLOADED
} else {
mangaChapter.chapter.status = Download.NOT_DOWNLOADED
} }
} }
@ -200,33 +85,89 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
* Get observable containing recent chapters and date * Get observable containing recent chapters and date
* @return observable containing recent chapters and date * @return observable containing recent chapters and date
*/ */
fun getRecentChaptersObservable(): Observable<ArrayList<Any>>? { fun getRecentChaptersObservable(): Observable<ArrayList<Any>> {
// Set date for recent chapters // Set date for recent chapters
val cal = Calendar.getInstance() val cal = Calendar.getInstance().apply {
cal.time = Date() time = Date()
cal.add(Calendar.MONTH, -1) add(Calendar.MONTH, -1)
}
return db.getRecentChapters(cal.time).asRxObservable() return db.getRecentChapters(cal.time).asRxObservable()
// Convert to a list of recent chapters.
.map { mangaChapters ->
mangaChapters.map { it.toModel() }
}
.doOnNext { chapters = it }
// Group chapters by the date they were fetched on a ordered map. // Group chapters by the date they were fetched on a ordered map.
.flatMap { recentItems -> .flatMap { recentItems ->
Observable.from(recentItems) Observable.from(recentItems)
.toMultimap( .toMultimap(
{ getMapKey(it.chapter.date_fetch) }, { getMapKey(it.date_fetch) },
{ it }, { it },
{ TreeMap { d1, d2 -> d2.compareTo(d1) } }) { TreeMap { d1, d2 -> d2.compareTo(d1) } })
} }
// Add every day and all its chapters to a single list. // Add every day and all its chapters to a single list.
.map { recentItems -> .map { recentItems ->
val items = ArrayList<Any>() ArrayList<Any>().apply {
recentItems.entries.forEach { recent -> for ((key, value) in recentItems) {
items.add(recent.key) add(key)
items.addAll(recent.value) addAll(value)
}
} }
items
} }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
/**
* Returns observable containing chapter status.
* @return download object containing download progress.
*/
private fun getChapterStatusObservable(): Observable<Download> {
return downloadManager.queue.getStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { download -> onDownloadStatusChange(download) }
}
/**
* Converts a chapter from the database to an extended model, allowing to store new fields.
*/
private fun MangaChapter.toModel(): RecentChapter {
// Create the model object.
val model = RecentChapter(this)
// Find an active download for this chapter.
val download = downloadManager.queue.find { it.chapter.id == chapter.id }
// If there's an active download, assign it, otherwise ask the manager if the chapter is
// downloaded and assign it to the status.
if (download != null) {
model.download = download
} else {
// Get source of chapter.
val source = sourceManager.get(manga.source)!!
model.status = if (downloadManager.isChapterDownloaded(source, manga, chapter))
Download.DOWNLOADED
else
Download.NOT_DOWNLOADED
}
return model
}
/**
* Update status of chapters.
* @param download download object containing progress.
*/
private fun onDownloadStatusChange(download: Download) {
// Assign the download to the model object.
if (download.status == Download.QUEUE) {
val chapter = chapters?.find { it.id == download.chapter.id }
if (chapter != null && chapter.download == null) {
chapter.download = download
}
}
}
/** /**
* Get date as time key * Get date as time key
* @param date desired date * @param date desired date
@ -244,18 +185,17 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Mark selected chapter as read * Mark selected chapter as read
* @param mangaChapters list of selected MangaChapters * @param chapters list of selected chapters
* @param read read status * @param read read status
*/ */
fun markChapterRead(mangaChapters: List<MangaChapter>, read: Boolean) { fun markChapterRead(chapters: List<RecentChapter>, read: Boolean) {
Observable.from(mangaChapters) Observable.from(chapters)
.doOnNext { mangaChapter -> .doOnNext { chapter ->
mangaChapter.chapter.read = read chapter.read = read
if (!read) { if (!read) {
mangaChapter.chapter.last_page_read = 0 chapter.last_page_read = 0
} }
} }
.map { mangaChapter -> mangaChapter.chapter }
.toList() .toList()
.flatMap { db.updateChaptersProgress(it).asRxObservable() } .flatMap { db.updateChaptersProgress(it).asRxObservable() }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -264,9 +204,9 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Delete selected chapters * Delete selected chapters
* @param chapters list of MangaChapters * @param chapters list of chapters
*/ */
fun deleteChapters(chapters: List<MangaChapter>) { fun deleteChapters(chapters: List<RecentChapter>) {
val wasRunning = downloadManager.isRunning val wasRunning = downloadManager.isRunning
if (wasRunning) { if (wasRunning) {
DownloadService.stop(context) DownloadService.stop(context)
@ -288,11 +228,11 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Download selected chapters * Download selected chapters
* @param mangaChapters [MangaChapter] that is selected * @param chapters list of recent chapters seleted.
*/ */
fun downloadChapters(mangaChapters: List<MangaChapter>) { fun downloadChapters(chapters: List<RecentChapter>) {
DownloadService.start(context) DownloadService.start(context)
Observable.from(mangaChapters) Observable.from(chapters)
.doOnNext { downloadChapter(it) } .doOnNext { downloadChapter(it) }
.subscribeOn(AndroidSchedulers.mainThread()) .subscribeOn(AndroidSchedulers.mainThread())
.subscribe() .subscribe()
@ -300,47 +240,23 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Download selected chapter * Download selected chapter
* @param item chapter that is selected * @param chapter chapter that is selected
*/ */
fun downloadChapter(item: MangaChapter) { fun downloadChapter(chapter: RecentChapter) {
DownloadService.start(context) DownloadService.start(context)
downloadManager.downloadChapters(item.manga, listOf(item.chapter)) downloadManager.downloadChapters(chapter.manga, listOf(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 * Delete selected chapter
* @param chapter chapter that is selected * @param chapter chapter that is selected
* @param manga manga that belongs to chapter
*/ */
private fun deleteChapter(chapter: Chapter, manga: Manga) { private fun deleteChapter(chapter: RecentChapter) {
val source = sourceManager.get(manga.source) ?: return val source = sourceManager.get(chapter.manga.source) ?: return
downloadManager.queue.del(chapter) downloadManager.queue.del(chapter)
downloadManager.deleteChapter(source, manga, chapter) downloadManager.deleteChapter(source, chapter.manga, chapter)
chapter.status = Download.NOT_DOWNLOADED chapter.status = Download.NOT_DOWNLOADED
chapter.download = null
} }
} }

View file

@ -2,11 +2,12 @@ package eu.kanade.tachiyomi.ui.recent_updates
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.text.format.DateUtils import android.text.format.DateUtils
import android.text.format.DateUtils.DAY_IN_MILLIS
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.item_recent_chapter_section.view.* import kotlinx.android.synthetic.main.item_recent_chapter_section.view.*
import java.util.* import java.util.*
class SectionViewHolder(view: View) : RecyclerView.ViewHolder(view) { class SectionViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
/** /**
* Current date * Current date
@ -19,8 +20,6 @@ class SectionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
* @param date of section header * @param date of section header
*/ */
fun onSetValues(date: Date) { fun onSetValues(date: Date) {
val s = DateUtils.getRelativeTimeSpanString( view.section_text.text = DateUtils.getRelativeTimeSpanString(date.time, now, DAY_IN_MILLIS)
date.time, now, DateUtils.DAY_IN_MILLIS)
itemView.section_text.text = s
} }
} }

View file

@ -37,8 +37,8 @@ class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter)
*/ */
fun onSetValues(item: MangaChapterHistory) { fun onSetValues(item: MangaChapterHistory) {
// Retrieve objects // Retrieve objects
val manga = item.mangaChapter.manga val manga = item.manga
val chapter = item.mangaChapter.chapter val chapter = item.chapter
val history = item.history val history = item.history
// Set manga title // Set manga title
@ -71,7 +71,7 @@ class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter)
.onPositive { materialDialog, dialogAction -> .onPositive { materialDialog, dialogAction ->
// Check if user wants all chapters reset // Check if user wants all chapters reset
if (materialDialog.customView?.removeAll?.isChecked as Boolean) { if (materialDialog.customView?.removeAll?.isChecked as Boolean) {
adapter.fragment.removeAllFromHistory(manga.id) adapter.fragment.removeAllFromHistory(manga.id!!)
} else { } else {
adapter.fragment.removeFromHistory(history) adapter.fragment.removeFromHistory(history)
} }

View file

@ -9,9 +9,9 @@ import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import javax.inject.Inject
/** /**
* Presenter of RecentlyReadFragment. * Presenter of RecentlyReadFragment.
@ -30,7 +30,7 @@ class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
/** /**
* Used to connect to database * Used to connect to database
*/ */
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)

View file

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.setting
import android.os.Bundle import android.os.Bundle
import android.support.v14.preference.PreferenceFragment import android.support.v14.preference.PreferenceFragment
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -12,22 +11,21 @@ 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.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import javax.inject.Inject import uy.kohesive.injekt.injectLazy
class SettingsActivity : BaseActivity() { class SettingsActivity : BaseActivity() {
@Inject lateinit var preferences: PreferencesHelper val preferences: PreferencesHelper by injectLazy()
@Inject lateinit var chapterCache: ChapterCache val chapterCache: ChapterCache by injectLazy()
@Inject lateinit var db: DatabaseHelper val db: DatabaseHelper by injectLazy()
@Inject lateinit var sourceManager: SourceManager val sourceManager: SourceManager by injectLazy()
@Inject lateinit var syncManager: MangaSyncManager val syncManager: MangaSyncManager by injectLazy()
@Inject lateinit var networkHelper: NetworkHelper val networkHelper: NetworkHelper by injectLazy()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
setAppTheme() setAppTheme()
super.onCreate(savedState) super.onCreate(savedState)
setContentView(R.layout.activity_preferences) setContentView(R.layout.activity_preferences)
App.get(this).component.inject(this)
setupToolbar(toolbar) setupToolbar(toolbar)

View file

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.util
import rx.Observable
import rx.functions.Func1
import java.util.concurrent.TimeUnit.MILLISECONDS
class RetryWithDelay(
private val maxRetries: Int = 1,
private val retryStrategy: (Int) -> Int = { 1000 }
) : Func1<Observable<out Throwable>, Observable<*>> {
private var retryCount = 0
override fun call(attempts: Observable<out Throwable>) = attempts.flatMap { error ->
val count = ++retryCount
if (count <= maxRetries) {
Observable.timer(retryStrategy(count).toLong(), MILLISECONDS)
} else {
Observable.error(error as Throwable)
}
}
}

View file

@ -30,7 +30,6 @@
<string name="pref_custom_brightness_value_key">pref_custom_brightness_value_key</string> <string name="pref_custom_brightness_value_key">pref_custom_brightness_value_key</string>
<string name="pref_reader_theme_key">pref_reader_theme_key</string> <string name="pref_reader_theme_key">pref_reader_theme_key</string>
<string name="pref_image_decoder_key">pref_image_decoder_key</string> <string name="pref_image_decoder_key">pref_image_decoder_key</string>
<string name="pref_seamless_mode_key">pref_seamless_mode_key</string>
<string name="pref_read_with_volume_keys_key">reader_volume_keys</string> <string name="pref_read_with_volume_keys_key">reader_volume_keys</string>
<string name="pref_read_with_tapping_key">reader_tap</string> <string name="pref_read_with_tapping_key">reader_tap</string>
<string name="pref_reencode_key">reencode_image</string> <string name="pref_reencode_key">reencode_image</string>

Some files were not shown because too many files have changed in this diff Show more