Browse Source

Use kapt, remove retrolambda, migrate database and source to Kotlin

len 10 năm trước cách đây
mục cha
commit
14f248546a
30 tập tin đã thay đổi với 878 bổ sung1042 xóa
  1. 4 16
      app/build.gradle
  2. 0 85
      app/src/main/java/eu/kanade/tachiyomi/App.java
  3. 70 0
      app/src/main/java/eu/kanade/tachiyomi/App.kt
  4. 0 424
      app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.java
  5. 303 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt
  6. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
  7. 0 2
      app/src/main/java/eu/kanade/tachiyomi/data/source/base/LoginSource.java
  8. 0 256
      app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.java
  9. 230 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.kt
  10. 25 15
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.java
  11. 4 4
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.java
  12. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.java
  13. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.java
  14. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.java
  15. 0 62
      app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java
  16. 55 0
      app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.kt
  17. 0 28
      app/src/main/java/eu/kanade/tachiyomi/injection/module/AppModule.java
  18. 21 0
      app/src/main/java/eu/kanade/tachiyomi/injection/module/AppModule.kt
  19. 0 73
      app/src/main/java/eu/kanade/tachiyomi/injection/module/DataModule.java
  20. 70 0
      app/src/main/java/eu/kanade/tachiyomi/injection/module/DataModule.kt
  21. 9 6
      app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.java
  22. 9 6
      app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.java
  23. 12 3
      app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/RxPresenter.java
  24. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
  25. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt
  26. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
  27. 33 23
      app/src/main/java/eu/kanade/tachiyomi/util/DynamicConcurrentMergeOperator.java
  28. 0 26
      app/src/main/java/eu/kanade/tachiyomi/util/RxPager.java
  29. 21 0
      app/src/main/java/eu/kanade/tachiyomi/util/RxPager.kt
  30. 0 1
      build.gradle

+ 4 - 16
app/build.gradle

@@ -4,11 +4,6 @@ apply plugin: 'com.android.application'
 apply plugin: 'kotlin-android'
 apply plugin: 'kotlin-android-extensions'
 apply plugin: 'com.neenbedankt.android-apt'
-apply plugin: 'me.tatarka.retrolambda'
-
-retrolambda {
-    jvmArgs '-noverify'
-}
 
 ext {
     // Git is needed in your system PATH for these commands to work.
@@ -55,11 +50,6 @@ android {
         vectorDrawables.useSupportLibrary = true
     }
 
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
-
     buildTypes {
         debug {
             applicationIdSuffix ".debug"
@@ -93,10 +83,8 @@ android {
 
 }
 
-apt {
-    arguments {
-        eventBusIndex "eu.kanade.tachiyomi.EventBusIndex"
-    }
+kapt {
+    generateStubs = true
 }
 
 dependencies {
@@ -146,8 +134,8 @@ dependencies {
     apt "org.greenrobot:eventbus-annotation-processor:3.0.1"
 
     compile "com.google.dagger:dagger:$DAGGER_VERSION"
-    apt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
-    apt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
+    kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
+    kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
     provided 'org.glassfish:javax.annotation:10.0-b28'
 
     compile('com.github.afollestad.material-dialogs:core:0.8.5.7@aar') {

+ 0 - 85
app/src/main/java/eu/kanade/tachiyomi/App.java

@@ -1,85 +0,0 @@
-package eu.kanade.tachiyomi;
-
-import android.app.Application;
-import android.content.Context;
-
-import org.acra.ACRA;
-import org.acra.annotation.ReportsCrashes;
-import org.greenrobot.eventbus.EventBus;
-
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
-import eu.kanade.tachiyomi.injection.ComponentReflectionInjector;
-import eu.kanade.tachiyomi.injection.component.AppComponent;
-import eu.kanade.tachiyomi.injection.component.DaggerAppComponent;
-import eu.kanade.tachiyomi.injection.module.AppModule;
-import timber.log.Timber;
-
-@ReportsCrashes(
-        formUri = "http://tachiyomi.kanade.eu/crash_report",
-        reportType = org.acra.sender.HttpSender.Type.JSON,
-        httpMethod = org.acra.sender.HttpSender.Method.PUT,
-        buildConfigClass = BuildConfig.class,
-        excludeMatchingSharedPreferencesKeys={".*username.*",".*password.*"}
-)
-public class App extends Application {
-
-    AppComponent applicationComponent;
-    ComponentReflectionInjector<AppComponent> componentInjector;
-
-    private int theme = 0;
-
-    public static App get(Context context) {
-        return (App) context.getApplicationContext();
-    }
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree());
-
-        applicationComponent = prepareAppComponent().build();
-
-        componentInjector =
-                new ComponentReflectionInjector<>(AppComponent.class, applicationComponent);
-
-        setupTheme();
-        setupEventBus();
-        setupAcra();
-    }
-
-    private void setupTheme() {
-        theme = PreferencesHelper.getTheme(this);
-    }
-
-    protected DaggerAppComponent.Builder prepareAppComponent() {
-        return DaggerAppComponent.builder()
-                .appModule(new AppModule(this));
-    }
-
-    protected void setupEventBus() {
-        EventBus.builder()
-//                .addIndex(new EventBusIndex())
-                .logNoSubscriberMessages(false)
-                .installDefaultEventBus();
-    }
-
-    protected void setupAcra() {
-        ACRA.init(this);
-    }
-
-    public AppComponent getComponent() {
-        return applicationComponent;
-    }
-
-    public ComponentReflectionInjector<AppComponent> getComponentReflection() {
-        return componentInjector;
-    }
-
-    public int getAppTheme() {
-        return theme;
-    }
-
-    public void setAppTheme(int theme) {
-        this.theme = theme;
-    }
-}

+ 70 - 0
app/src/main/java/eu/kanade/tachiyomi/App.kt

@@ -0,0 +1,70 @@
+package eu.kanade.tachiyomi
+
+import android.app.Application
+import android.content.Context
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.injection.ComponentReflectionInjector
+import eu.kanade.tachiyomi.injection.component.AppComponent
+import eu.kanade.tachiyomi.injection.component.DaggerAppComponent
+import eu.kanade.tachiyomi.injection.module.AppModule
+import org.acra.ACRA
+import org.acra.annotation.ReportsCrashes
+import org.greenrobot.eventbus.EventBus
+import timber.log.Timber
+
+@ReportsCrashes(
+        formUri = "http://tachiyomi.kanade.eu/crash_report",
+        reportType = org.acra.sender.HttpSender.Type.JSON,
+        httpMethod = org.acra.sender.HttpSender.Method.PUT,
+        buildConfigClass = BuildConfig::class,
+        excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*")
+)
+open class App : Application() {
+
+    lateinit var component: AppComponent
+        private set
+
+    lateinit var componentReflection: ComponentReflectionInjector<AppComponent>
+        private set
+
+    var appTheme = 0
+
+    override fun onCreate() {
+        super.onCreate()
+        if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
+
+        component = prepareAppComponent().build()
+
+        componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
+
+        setupTheme()
+        setupEventBus()
+        setupAcra()
+    }
+
+    private fun setupTheme() {
+        appTheme = PreferencesHelper.getTheme(this)
+    }
+
+    protected open fun prepareAppComponent(): DaggerAppComponent.Builder {
+        return DaggerAppComponent.builder()
+                .appModule(AppModule(this))
+    }
+
+    protected open fun setupEventBus() {
+        EventBus.builder()
+                .logNoSubscriberMessages(false)
+                .installDefaultEventBus()
+    }
+
+    protected open fun setupAcra() {
+        ACRA.init(this)
+    }
+
+    companion object {
+        @JvmStatic
+        fun get(context: Context): App {
+            return context.applicationContext as App
+        }
+    }
+}

+ 0 - 424
app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.java

@@ -1,424 +0,0 @@
-package eu.kanade.tachiyomi.data.database;
-
-import android.content.Context;
-import android.util.Pair;
-
-import com.pushtorefresh.storio.Queries;
-import com.pushtorefresh.storio.sqlite.StorIOSQLite;
-import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite;
-import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteByQuery;
-import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteCollectionOfObjects;
-import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteObject;
-import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects;
-import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject;
-import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutCollectionOfObjects;
-import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutObject;
-import com.pushtorefresh.storio.sqlite.queries.DeleteQuery;
-import com.pushtorefresh.storio.sqlite.queries.Query;
-import com.pushtorefresh.storio.sqlite.queries.RawQuery;
-
-import java.util.Date;
-import java.util.List;
-import java.util.TreeSet;
-
-import eu.kanade.tachiyomi.data.database.models.Category;
-import eu.kanade.tachiyomi.data.database.models.CategorySQLiteTypeMapping;
-import eu.kanade.tachiyomi.data.database.models.Chapter;
-import eu.kanade.tachiyomi.data.database.models.ChapterSQLiteTypeMapping;
-import eu.kanade.tachiyomi.data.database.models.Manga;
-import eu.kanade.tachiyomi.data.database.models.MangaCategory;
-import eu.kanade.tachiyomi.data.database.models.MangaCategorySQLiteTypeMapping;
-import eu.kanade.tachiyomi.data.database.models.MangaChapter;
-import eu.kanade.tachiyomi.data.database.models.MangaSQLiteTypeMapping;
-import eu.kanade.tachiyomi.data.database.models.MangaSync;
-import eu.kanade.tachiyomi.data.database.models.MangaSyncSQLiteTypeMapping;
-import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver;
-import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver;
-import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
-import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
-import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
-import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
-import eu.kanade.tachiyomi.data.database.tables.MangaTable;
-import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
-import eu.kanade.tachiyomi.data.source.base.Source;
-import eu.kanade.tachiyomi.util.ChapterRecognition;
-import rx.Observable;
-
-public class DatabaseHelper {
-
-    private StorIOSQLite db;
-
-    public DatabaseHelper(Context context) {
-
-        db = DefaultStorIOSQLite.builder()
-                .sqliteOpenHelper(new DbOpenHelper(context))
-                .addTypeMapping(Manga.class, new MangaSQLiteTypeMapping())
-                .addTypeMapping(Chapter.class, new ChapterSQLiteTypeMapping())
-                .addTypeMapping(MangaSync.class, new MangaSyncSQLiteTypeMapping())
-                .addTypeMapping(Category.class, new CategorySQLiteTypeMapping())
-                .addTypeMapping(MangaCategory.class, new MangaCategorySQLiteTypeMapping())
-                .build();
-    }
-
-    // Mangas related queries
-
-    public PreparedGetListOfObjects<Manga> getMangas() {
-        return db.get()
-                .listOfObjects(Manga.class)
-                .withQuery(Query.builder()
-                        .table(MangaTable.TABLE)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedGetListOfObjects<Manga> getLibraryMangas() {
-        return db.get()
-                .listOfObjects(Manga.class)
-                .withQuery(RawQuery.builder()
-                        .query(RawQueriesKt.getLibraryQuery())
-                        .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
-                        .build())
-                .withGetResolver(LibraryMangaGetResolver.INSTANCE)
-                .prepare();
-    }
-
-    public PreparedGetListOfObjects<Manga> getFavoriteMangas() {
-        return db.get()
-                .listOfObjects(Manga.class)
-                .withQuery(Query.builder()
-                        .table(MangaTable.TABLE)
-                        .where(MangaTable.COLUMN_FAVORITE + "=?")
-                        .whereArgs(1)
-                        .orderBy(MangaTable.COLUMN_TITLE)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedGetObject<Manga> getManga(String url, int sourceId) {
-        return db.get()
-                .object(Manga.class)
-                .withQuery(Query.builder()
-                        .table(MangaTable.TABLE)
-                        .where(MangaTable.COLUMN_URL + "=? AND " + MangaTable.COLUMN_SOURCE + "=?")
-                        .whereArgs(url, sourceId)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedGetObject<Manga> getManga(long id) {
-        return db.get()
-                .object(Manga.class)
-                .withQuery(Query.builder()
-                        .table(MangaTable.TABLE)
-                        .where(MangaTable.COLUMN_ID + "=?")
-                        .whereArgs(id)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedPutObject<Manga> insertManga(Manga manga) {
-        return db.put()
-                .object(manga)
-                .prepare();
-    }
-
-    public PreparedPutCollectionOfObjects<Manga> insertMangas(List<Manga> mangas) {
-        return db.put()
-                .objects(mangas)
-                .prepare();
-    }
-
-    public PreparedDeleteObject<Manga> deleteManga(Manga manga) {
-        return db.delete()
-                .object(manga)
-                .prepare();
-    }
-
-    public PreparedDeleteCollectionOfObjects<Manga> deleteMangas(List<Manga> mangas) {
-        return db.delete()
-                .objects(mangas)
-                .prepare();
-    }
-
-    public PreparedDeleteByQuery deleteMangasNotInLibrary() {
-        return db.delete()
-                .byQuery(DeleteQuery.builder()
-                        .table(MangaTable.TABLE)
-                        .where(MangaTable.COLUMN_FAVORITE + "=?")
-                        .whereArgs(0)
-                        .build())
-                .prepare();
-    }
-
-
-    // Chapters related queries
-
-    public PreparedGetListOfObjects<Chapter> getChapters(Manga manga) {
-        return db.get()
-                .listOfObjects(Chapter.class)
-                .withQuery(Query.builder()
-                        .table(ChapterTable.TABLE)
-                        .where(ChapterTable.COLUMN_MANGA_ID + "=?")
-                        .whereArgs(manga.id)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedGetListOfObjects<MangaChapter> getRecentChapters(Date date) {
-        return db.get()
-                .listOfObjects(MangaChapter.class)
-                .withQuery(RawQuery.builder()
-                        .query(RawQueriesKt.getRecentsQuery(date))
-                        .observesTables(ChapterTable.TABLE)
-                        .build())
-                .withGetResolver(MangaChapterGetResolver.INSTANCE)
-                .prepare();
-    }
-
-    public PreparedGetObject<Chapter> getNextChapter(Chapter chapter) {
-        // Add a delta to the chapter number, because binary decimal representation
-        // can retrieve the same chapter again
-        double chapterNumber = chapter.chapter_number + 0.00001;
-
-        return db.get()
-                .object(Chapter.class)
-                .withQuery(Query.builder()
-                        .table(ChapterTable.TABLE)
-                        .where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
-                                ChapterTable.COLUMN_CHAPTER_NUMBER + ">? AND " +
-                                ChapterTable.COLUMN_CHAPTER_NUMBER + "<=?")
-                        .whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
-                        .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
-                        .limit(1)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedGetObject<Chapter> getPreviousChapter(Chapter chapter) {
-        // Add a delta to the chapter number, because binary decimal representation
-        // can retrieve the same chapter again
-        double chapterNumber = chapter.chapter_number - 0.00001;
-
-        return db.get()
-                .object(Chapter.class)
-                .withQuery(Query.builder()
-                        .table(ChapterTable.TABLE)
-                        .where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
-                                ChapterTable.COLUMN_CHAPTER_NUMBER + "<? AND " +
-                                ChapterTable.COLUMN_CHAPTER_NUMBER + ">=?")
-                        .whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
-                        .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
-                        .limit(1)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedGetObject<Chapter> getNextUnreadChapter(Manga manga) {
-        return db.get()
-                .object(Chapter.class)
-                .withQuery(Query.builder()
-                        .table(ChapterTable.TABLE)
-                        .where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
-                                ChapterTable.COLUMN_READ + "=? AND " +
-                                ChapterTable.COLUMN_CHAPTER_NUMBER + ">=?")
-                        .whereArgs(manga.id, 0, 0)
-                        .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
-                        .limit(1)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedPutObject<Chapter> insertChapter(Chapter chapter) {
-        return db.put()
-                .object(chapter)
-                .prepare();
-    }
-
-    public PreparedPutCollectionOfObjects<Chapter> insertChapters(List<Chapter> chapters) {
-        return db.put()
-                .objects(chapters)
-                .prepare();
-    }
-
-    // Add new chapters or delete if the source deletes them
-    public Observable<Pair<Integer, Integer>> insertOrRemoveChapters(Manga manga, List<Chapter> sourceChapters, Source source) {
-        List<Chapter> dbChapters = getChapters(manga).executeAsBlocking();
-
-        Observable<List<Chapter>> newChapters = Observable.from(sourceChapters)
-                .filter(c -> !dbChapters.contains(c))
-                .doOnNext(c -> {
-                    c.manga_id = manga.id;
-                    source.parseChapterNumber(c);
-                    ChapterRecognition.parseChapterNumber(c, manga);
-                })
-                .toList();
-
-        Observable<List<Chapter>> deletedChapters = Observable.from(dbChapters)
-                .filter(c -> !sourceChapters.contains(c))
-                .toList();
-
-        return Observable.zip(newChapters, deletedChapters, (toAdd, toDelete) -> {
-            int added = 0;
-            int deleted = 0;
-            int readded = 0;
-            db.internal().beginTransaction();
-            try {
-                TreeSet<Float> deletedReadChapterNumbers = new TreeSet<>();
-                if (!toDelete.isEmpty()) {
-                    for (Chapter c : toDelete) {
-                        if (c.read) {
-                            deletedReadChapterNumbers.add(c.chapter_number);
-                        }
-                    }
-                    deleted = deleteChapters(toDelete).executeAsBlocking().results().size();
-                }
-
-                if (!toAdd.isEmpty()) {
-                    // Set the date fetch for new items in reverse order to allow another sorting method.
-                    // Sources MUST return the chapters from most to less recent, which is common.
-                    long now = new Date().getTime();
-
-                    for (int i = toAdd.size() - 1; i >= 0; i--) {
-                        Chapter c = toAdd.get(i);
-                        c.date_fetch = now++;
-                        // Try to mark already read chapters as read when the source deletes them
-                        if (c.chapter_number != -1 && deletedReadChapterNumbers.contains(c.chapter_number)) {
-                            c.read = true;
-                            readded++;
-                        }
-                    }
-                    added = insertChapters(toAdd).executeAsBlocking().numberOfInserts();
-                }
-
-                db.internal().setTransactionSuccessful();
-            } finally {
-                db.internal().endTransaction();
-            }
-            return Pair.create(added - readded, deleted - readded);
-        });
-    }
-
-    public PreparedDeleteObject<Chapter> deleteChapter(Chapter chapter) {
-        return db.delete()
-                .object(chapter)
-                .prepare();
-    }
-
-    public PreparedDeleteCollectionOfObjects<Chapter> deleteChapters(List<Chapter> chapters) {
-        return db.delete()
-                .objects(chapters)
-                .prepare();
-    }
-
-    // Manga sync related queries
-
-    public PreparedGetObject<MangaSync> getMangaSync(Manga manga, MangaSyncService sync) {
-        return db.get()
-                .object(MangaSync.class)
-                .withQuery(Query.builder()
-                        .table(MangaSyncTable.TABLE)
-                        .where(MangaSyncTable.COLUMN_MANGA_ID + "=? AND " +
-                                MangaSyncTable.COLUMN_SYNC_ID + "=?")
-                        .whereArgs(manga.id, sync.getId())
-                        .build())
-                .prepare();
-    }
-
-    public PreparedGetListOfObjects<MangaSync> getMangasSync(Manga manga) {
-        return db.get()
-                .listOfObjects(MangaSync.class)
-                .withQuery(Query.builder()
-                        .table(MangaSyncTable.TABLE)
-                        .where(MangaSyncTable.COLUMN_MANGA_ID + "=?")
-                        .whereArgs(manga.id)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedPutObject<MangaSync> insertMangaSync(MangaSync manga) {
-        return db.put()
-                .object(manga)
-                .prepare();
-    }
-
-    public PreparedDeleteObject<MangaSync> deleteMangaSync(MangaSync manga) {
-        return db.delete()
-                .object(manga)
-                .prepare();
-    }
-
-    // Categories related queries
-
-    public PreparedGetListOfObjects<Category> getCategories() {
-        return db.get()
-                .listOfObjects(Category.class)
-                .withQuery(Query.builder()
-                        .table(CategoryTable.TABLE)
-                        .orderBy(CategoryTable.COLUMN_ORDER)
-                        .build())
-                .prepare();
-    }
-
-    public PreparedPutObject<Category> insertCategory(Category category) {
-        return db.put()
-                .object(category)
-                .prepare();
-    }
-
-    public PreparedPutCollectionOfObjects<Category> insertCategories(List<Category> categories) {
-        return db.put()
-                .objects(categories)
-                .prepare();
-    }
-
-    public PreparedDeleteObject<Category> deleteCategory(Category category) {
-        return db.delete()
-                .object(category)
-                .prepare();
-    }
-
-    public PreparedDeleteCollectionOfObjects<Category> deleteCategories(List<Category> categories) {
-        return db.delete()
-                .objects(categories)
-                .prepare();
-    }
-
-    public PreparedPutObject<MangaCategory> insertMangaCategory(MangaCategory mangaCategory) {
-        return db.put()
-                .object(mangaCategory)
-                .prepare();
-    }
-
-    public PreparedPutCollectionOfObjects<MangaCategory> insertMangasCategories(List<MangaCategory> mangasCategories) {
-        return db.put()
-                .objects(mangasCategories)
-                .prepare();
-    }
-
-    public PreparedDeleteByQuery deleteOldMangasCategories(List<Manga> mangas) {
-        List<Long> mangaIds = Observable.from(mangas)
-                .map(manga -> manga.id)
-                .toList().toBlocking().single();
-
-        return db.delete()
-                .byQuery(DeleteQuery.builder()
-                        .table(MangaCategoryTable.TABLE)
-                        .where(MangaCategoryTable.COLUMN_MANGA_ID + " IN ("
-                                + Queries.placeholders(mangas.size()) + ")")
-                        .whereArgs(mangaIds.toArray())
-                        .build())
-                .prepare();
-    }
-
-    public void setMangaCategories(List<MangaCategory> mangasCategories, List<Manga> mangas) {
-        db.internal().beginTransaction();
-        try {
-            deleteOldMangasCategories(mangas).executeAsBlocking();
-            insertMangasCategories(mangasCategories).executeAsBlocking();
-            db.internal().setTransactionSuccessful();
-        } finally {
-            db.internal().endTransaction();
-        }
-    }
-
-}

+ 303 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt

@@ -0,0 +1,303 @@
+package eu.kanade.tachiyomi.data.database
+
+import android.content.Context
+import android.util.Pair
+import com.pushtorefresh.storio.Queries
+import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
+import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteByQuery
+import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
+import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
+import com.pushtorefresh.storio.sqlite.queries.Query
+import com.pushtorefresh.storio.sqlite.queries.RawQuery
+import eu.kanade.tachiyomi.data.database.models.*
+import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
+import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
+import eu.kanade.tachiyomi.data.database.tables.*
+import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
+import eu.kanade.tachiyomi.data.source.base.Source
+import eu.kanade.tachiyomi.util.ChapterRecognition
+import rx.Observable
+import java.util.*
+
+class DatabaseHelper(context: Context) {
+
+    val db = DefaultStorIOSQLite.builder()
+            .sqliteOpenHelper(DbOpenHelper(context))
+            .addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
+            .addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
+            .addTypeMapping(MangaSync::class.java, MangaSyncSQLiteTypeMapping())
+            .addTypeMapping(Category::class.java, CategorySQLiteTypeMapping())
+            .addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
+            .build()
+
+    inline fun inTransaction(func: DatabaseHelper.() -> Unit) {
+        db.internal().beginTransaction()
+        try {
+            func()
+            db.internal().setTransactionSuccessful()
+        } finally {
+            db.internal().endTransaction()
+        }
+    }
+
+    // Mangas related queries
+
+    fun getMangas() = db.get()
+            .listOfObjects(Manga::class.java)
+            .withQuery(Query.builder()
+                    .table(MangaTable.TABLE)
+                    .build())
+            .prepare()
+
+    fun getLibraryMangas() = db.get()
+            .listOfObjects(Manga::class.java)
+            .withQuery(RawQuery.builder()
+                    .query(libraryQuery)
+                    .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
+                    .build())
+            .withGetResolver(LibraryMangaGetResolver.INSTANCE)
+            .prepare()
+
+    fun getFavoriteMangas() = db.get()
+            .listOfObjects(Manga::class.java)
+            .withQuery(Query.builder()
+                    .table(MangaTable.TABLE)
+                    .where("${MangaTable.COLUMN_FAVORITE} = ?")
+                    .whereArgs(1)
+                    .orderBy(MangaTable.COLUMN_TITLE)
+                    .build())
+            .prepare()
+
+    fun getManga(url: String, sourceId: Int) = db.get()
+            .`object`(Manga::class.java)
+            .withQuery(Query.builder()
+                    .table(MangaTable.TABLE)
+                    .where("${MangaTable.COLUMN_URL} = ? AND ${MangaTable.COLUMN_SOURCE} = ?")
+                    .whereArgs(url, sourceId)
+                    .build())
+            .prepare()
+
+    fun getManga(id: Long) = db.get()
+            .`object`(Manga::class.java)
+            .withQuery(Query.builder()
+                    .table(MangaTable.TABLE)
+                    .where("${MangaTable.COLUMN_ID} = ?")
+                    .whereArgs(id)
+                    .build())
+            .prepare()
+
+    fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
+
+    fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
+
+    fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
+
+    fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
+
+    fun deleteMangasNotInLibrary() = db.delete()
+            .byQuery(DeleteQuery.builder()
+                    .table(MangaTable.TABLE)
+                    .where("${MangaTable.COLUMN_FAVORITE} = ?")
+                    .whereArgs(0)
+                    .build())
+            .prepare()
+
+
+    // Chapters related queries
+
+    fun getChapters(manga: Manga) = db.get()
+            .listOfObjects(Chapter::class.java)
+            .withQuery(Query.builder()
+                    .table(ChapterTable.TABLE)
+                    .where("${ChapterTable.COLUMN_MANGA_ID} = ?")
+                    .whereArgs(manga.id)
+                    .build())
+            .prepare()
+
+    fun getRecentChapters(date: Date) = db.get()
+            .listOfObjects(MangaChapter::class.java)
+            .withQuery(RawQuery.builder()
+                    .query(getRecentsQuery(date))
+                    .observesTables(ChapterTable.TABLE)
+                    .build())
+            .withGetResolver(MangaChapterGetResolver.INSTANCE)
+            .prepare()
+
+    fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
+        // Add a delta to the chapter number, because binary decimal representation
+        // can retrieve the same chapter again
+        val chapterNumber = chapter.chapter_number + 0.00001
+
+        return db.get()
+                .`object`(Chapter::class.java)
+                .withQuery(Query.builder()
+                        .table(ChapterTable.TABLE)
+                        .where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
+                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} > ? AND " +
+                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} <= ?")
+                        .whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
+                        .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
+                        .limit(1)
+                        .build())
+                .prepare()
+    }
+
+    fun getPreviousChapter(chapter: Chapter): PreparedGetObject<Chapter> {
+        // Add a delta to the chapter number, because binary decimal representation
+        // can retrieve the same chapter again
+        val chapterNumber = chapter.chapter_number - 0.00001
+
+        return db.get()
+                .`object`(Chapter::class.java)
+                .withQuery(Query.builder().table(ChapterTable.TABLE)
+                        .where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
+                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} < ? AND " +
+                                "${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
+                        .whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
+                        .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
+                        .limit(1)
+                        .build())
+                .prepare()
+    }
+
+    fun getNextUnreadChapter(manga: Manga) = db.get()
+            .`object`(Chapter::class.java)
+            .withQuery(Query.builder()
+                    .table(ChapterTable.TABLE)
+                    .where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
+                            "${ChapterTable.COLUMN_READ} = ? AND " +
+                            "${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
+                    .whereArgs(manga.id, 0, 0)
+                    .orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
+                    .limit(1)
+                    .build())
+            .prepare()
+
+    fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
+
+    fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
+
+    // Add new chapters or delete if the source deletes them
+    fun insertOrRemoveChapters(manga: Manga, sourceChapters: List<Chapter>, source: Source): Observable<Pair<Int, Int>> {
+        val dbChapters = getChapters(manga).executeAsBlocking()
+
+        val newChapters = Observable.from(sourceChapters)
+                .filter { it !in dbChapters }
+                .doOnNext { c ->
+                    c.manga_id = manga.id
+                    source.parseChapterNumber(c)
+                    ChapterRecognition.parseChapterNumber(c, manga)
+                }.toList()
+
+        val deletedChapters = Observable.from(dbChapters)
+                .filter { it !in sourceChapters }
+                .toList()
+
+        return Observable.zip(newChapters, deletedChapters) { toAdd, toDelete ->
+            var added = 0
+            var deleted = 0
+            var readded = 0
+
+            inTransaction {
+                val deletedReadChapterNumbers = TreeSet<Float>()
+                if (!toDelete.isEmpty()) {
+                    for (c in toDelete) {
+                        if (c.read) {
+                            deletedReadChapterNumbers.add(c.chapter_number)
+                        }
+                    }
+                    deleted = deleteChapters(toDelete).executeAsBlocking().results().size
+                }
+
+                if (!toAdd.isEmpty()) {
+                    // Set the date fetch for new items in reverse order to allow another sorting method.
+                    // Sources MUST return the chapters from most to less recent, which is common.
+                    var now = Date().time
+
+                    for (i in toAdd.indices.reversed()) {
+                        val c = toAdd[i]
+                        c.date_fetch = now++
+                        // Try to mark already read chapters as read when the source deletes them
+                        if (c.chapter_number != -1f && c.chapter_number in deletedReadChapterNumbers) {
+                            c.read = true
+                            readded++
+                        }
+                    }
+                    added = insertChapters(toAdd).executeAsBlocking().numberOfInserts()
+                }
+            }
+            Pair.create(added - readded, deleted - readded)
+        }
+    }
+
+    fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
+
+    fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
+
+    // Manga sync related queries
+
+    fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
+            .`object`(MangaSync::class.java)
+            .withQuery(Query.builder()
+                    .table(MangaSyncTable.TABLE)
+                    .where("${MangaSyncTable.COLUMN_MANGA_ID} = ? AND " +
+                            "${MangaSyncTable.COLUMN_SYNC_ID} = ?")
+                    .whereArgs(manga.id, sync.id)
+                    .build())
+            .prepare()
+
+    fun getMangasSync(manga: Manga) = db.get()
+            .listOfObjects(MangaSync::class.java)
+            .withQuery(Query.builder()
+                    .table(MangaSyncTable.TABLE)
+                    .where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
+                    .whereArgs(manga.id)
+                    .build())
+            .prepare()
+
+    fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
+
+    fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
+
+    // Categories related queries
+
+    fun getCategories() = db.get()
+            .listOfObjects(Category::class.java)
+            .withQuery(Query.builder()
+                    .table(CategoryTable.TABLE)
+                    .orderBy(CategoryTable.COLUMN_ORDER)
+                    .build())
+            .prepare()
+
+    fun insertCategory(category: Category) = db.put().`object`(category).prepare()
+
+    fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
+
+    fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
+
+    fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
+
+    fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
+
+    fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
+
+    fun deleteOldMangasCategories(mangas: List<Manga>): PreparedDeleteByQuery {
+        val mangaIds = Observable.from(mangas).map { manga -> manga.id }.toList().toBlocking().single()
+
+        return db.delete()
+                .byQuery(DeleteQuery.builder()
+                        .table(MangaCategoryTable.TABLE)
+                        .where("${MangaCategoryTable.COLUMN_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
+                        .whereArgs(mangaIds)
+                        .build())
+                .prepare()
+    }
+
+    fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
+        inTransaction {
+            deleteOldMangasCategories(mangas).executeAsBlocking()
+            insertMangasCategories(mangasCategories).executeAsBlocking()
+        }
+    }
+
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt

@@ -168,7 +168,7 @@ class LibraryUpdateService : Service() {
                 Intent(this, CancelUpdateReceiver::class.java), 0)
 
         // Get the manga list that is going to be updated.
-        val allLibraryMangas = db.favoriteMangas.executeAsBlocking()
+        val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking()
         val toUpdate = if (!preferences.updateOnlyNonCompleted())
             allLibraryMangas
         else

+ 0 - 2
app/src/main/java/eu/kanade/tachiyomi/data/source/base/LoginSource.java

@@ -4,8 +4,6 @@ import android.content.Context;
 
 public abstract class LoginSource extends Source {
 
-    public LoginSource() {}
-
     public LoginSource(Context context) {
         super(context);
     }

+ 0 - 256
app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.java

@@ -1,256 +0,0 @@
-package eu.kanade.tachiyomi.data.source.base;
-
-import android.content.Context;
-
-import com.bumptech.glide.load.model.LazyHeaders;
-
-import org.jsoup.Jsoup;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import javax.inject.Inject;
-
-import eu.kanade.tachiyomi.App;
-import eu.kanade.tachiyomi.data.cache.ChapterCache;
-import eu.kanade.tachiyomi.data.database.models.Chapter;
-import eu.kanade.tachiyomi.data.database.models.Manga;
-import eu.kanade.tachiyomi.data.network.NetworkHelper;
-import eu.kanade.tachiyomi.data.network.ReqKt;
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
-import eu.kanade.tachiyomi.data.source.model.MangasPage;
-import eu.kanade.tachiyomi.data.source.model.Page;
-import okhttp3.Headers;
-import okhttp3.Request;
-import okhttp3.Response;
-import rx.Observable;
-import rx.schedulers.Schedulers;
-
-public abstract class Source extends BaseSource {
-
-    @Inject protected NetworkHelper networkService;
-    @Inject protected ChapterCache chapterCache;
-    @Inject protected PreferencesHelper prefs;
-    protected Headers requestHeaders;
-    protected LazyHeaders glideHeaders;
-
-    public Source() {}
-
-    public Source(Context context) {
-        App.get(context).getComponent().inject(this);
-        requestHeaders = headersBuilder().build();
-        glideHeaders = glideHeadersBuilder().build();
-    }
-
-    @Override
-    public boolean isLoginRequired() {
-        return false;
-    }
-
-    protected Request popularMangaRequest(MangasPage page) {
-        if (page.page == 1) {
-            page.url = getInitialPopularMangasUrl();
-        }
-
-        return ReqKt.get(page.url, requestHeaders);
-    }
-
-    protected Request searchMangaRequest(MangasPage page, String query) {
-        if (page.page == 1) {
-            page.url = getInitialSearchUrl(query);
-        }
-
-        return ReqKt.get(page.url, requestHeaders);
-    }
-
-    protected Request mangaDetailsRequest(String mangaUrl) {
-        return ReqKt.get(getBaseUrl() + mangaUrl, requestHeaders);
-    }
-
-    protected Request chapterListRequest(String mangaUrl) {
-        return ReqKt.get(getBaseUrl() + mangaUrl, requestHeaders);
-    }
-
-    protected Request pageListRequest(String chapterUrl) {
-        return ReqKt.get(getBaseUrl() + chapterUrl, requestHeaders);
-    }
-
-    protected Request imageUrlRequest(Page page) {
-        return ReqKt.get(page.getUrl(), requestHeaders);
-    }
-
-    protected Request imageRequest(Page page) {
-        return ReqKt.get(page.getImageUrl(), requestHeaders);
-    }
-
-    // Get the most popular mangas from the source
-    public Observable<MangasPage> pullPopularMangasFromNetwork(MangasPage page) {
-        return networkService
-                .requestBody(popularMangaRequest(page), true)
-                .map(Jsoup::parse)
-                .doOnNext(doc -> page.mangas = parsePopularMangasFromHtml(doc))
-                .doOnNext(doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page))
-                .map(response -> page);
-    }
-
-    // Get mangas from the source with a query
-    public Observable<MangasPage> searchMangasFromNetwork(MangasPage page, String query) {
-        return networkService
-                .requestBody(searchMangaRequest(page, query), true)
-                .map(Jsoup::parse)
-                .doOnNext(doc -> page.mangas = parseSearchFromHtml(doc))
-                .doOnNext(doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query))
-                .map(response -> page);
-    }
-
-    // Get manga details from the source
-    public Observable<Manga> pullMangaFromNetwork(final String mangaUrl) {
-        return networkService
-                .requestBody(mangaDetailsRequest(mangaUrl))
-                .flatMap(unparsedHtml -> Observable.just(parseHtmlToManga(mangaUrl, unparsedHtml)));
-    }
-
-    // Get chapter list of a manga from the source
-    public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
-        return networkService
-                .requestBody(chapterListRequest(mangaUrl))
-                .flatMap(unparsedHtml -> {
-                    List<Chapter> chapters = parseHtmlToChapters(unparsedHtml);
-                    return !chapters.isEmpty() ?
-                            Observable.just(chapters) :
-                            Observable.error(new Exception("No chapters found"));
-                });
-    }
-
-    public Observable<List<Page>> getCachedPageListOrPullFromNetwork(final String chapterUrl) {
-        return chapterCache.getPageListFromCache(getChapterCacheKey(chapterUrl))
-                .onErrorResumeNext(throwable -> {
-                    return pullPageListFromNetwork(chapterUrl);
-                })
-                .onBackpressureBuffer();
-    }
-
-    public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
-        return networkService
-                .requestBody(pageListRequest(chapterUrl))
-                .flatMap(unparsedHtml -> {
-                    List<Page> pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
-                    return !pages.isEmpty() ?
-                            Observable.just(parseFirstPage(pages, unparsedHtml)) :
-                            Observable.error(new Exception("Page list is empty"));
-                });
-    }
-
-    public Observable<Page> getAllImageUrlsFromPageList(final List<Page> pages) {
-        return Observable.from(pages)
-                .filter(page -> page.getImageUrl() != null)
-                .mergeWith(getRemainingImageUrlsFromPageList(pages));
-    }
-
-    // Get the URLs of the images of a chapter
-    public Observable<Page> getRemainingImageUrlsFromPageList(final List<Page> pages) {
-        return Observable.from(pages)
-                .filter(page -> page.getImageUrl() == null)
-                .concatMap(this::getImageUrlFromPage);
-    }
-
-    public Observable<Page> getImageUrlFromPage(final Page page) {
-        page.setStatus(Page.LOAD_PAGE);
-        return networkService
-                .requestBody(imageUrlRequest(page))
-                .flatMap(unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)))
-                .onErrorResumeNext(e -> {
-                    page.setStatus(Page.ERROR);
-                    return Observable.just(null);
-                })
-                .flatMap(imageUrl -> {
-                    page.setImageUrl(imageUrl);
-                    return Observable.just(page);
-                })
-                .subscribeOn(Schedulers.io());
-    }
-
-    public Observable<Page> getCachedImage(final Page page) {
-        Observable<Page> pageObservable = Observable.just(page);
-        if (page.getImageUrl() == null)
-            return pageObservable;
-
-        return pageObservable
-                .flatMap(p -> {
-                    if (!chapterCache.isImageInCache(page.getImageUrl())) {
-                        return cacheImage(page);
-                    }
-                    return Observable.just(page);
-                })
-                .flatMap(p -> {
-                    page.setImagePath(chapterCache.getImagePath(page.getImageUrl()));
-                    page.setStatus(Page.READY);
-                    return Observable.just(page);
-                })
-                .onErrorResumeNext(e -> {
-                    page.setStatus(Page.ERROR);
-                    return Observable.just(page);
-                });
-    }
-
-    private Observable<Page> cacheImage(final Page page) {
-        page.setStatus(Page.DOWNLOAD_IMAGE);
-        return getImageProgressResponse(page)
-                .flatMap(resp -> {
-                    try {
-                        chapterCache.putImageToCache(page.getImageUrl(), resp);
-                    } catch (IOException e) {
-                        return Observable.error(e);
-                    }
-                    return Observable.just(page);
-                });
-    }
-
-    public Observable<Response> getImageProgressResponse(final Page page) {
-        return networkService.requestBodyProgress(imageRequest(page), page);
-    }
-
-    public void savePageList(String chapterUrl, List<Page> pages) {
-        if (pages != null)
-            chapterCache.putPageListToCache(getChapterCacheKey(chapterUrl), pages);
-    }
-
-    protected List<Page> convertToPages(List<String> pageUrls) {
-        List<Page> pages = new ArrayList<>();
-        for (int i = 0; i < pageUrls.size(); i++) {
-            pages.add(new Page(i, pageUrls.get(i)));
-        }
-        return pages;
-    }
-
-    protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
-        String firstImage = parseHtmlToImageUrl(unparsedHtml);
-        pages.get(0).setImageUrl(firstImage);
-        return pages;
-    }
-
-    protected String getChapterCacheKey(String chapterUrl) {
-        return getId() + chapterUrl;
-    }
-
-    // Overridable method to allow custom parsing.
-    public void parseChapterNumber(Chapter chapter) {
-
-    }
-
-    protected LazyHeaders.Builder glideHeadersBuilder() {
-        LazyHeaders.Builder builder = new LazyHeaders.Builder();
-        for (Map.Entry<String, List<String>> entry : requestHeaders.toMultimap().entrySet()) {
-            builder.addHeader(entry.getKey(), entry.getValue().get(0));
-        }
-
-        return builder;
-    }
-
-    public LazyHeaders getGlideHeaders() {
-        return glideHeaders;
-    }
-
-}

+ 230 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.kt

@@ -0,0 +1,230 @@
+package eu.kanade.tachiyomi.data.source.base
+
+import android.content.Context
+import com.bumptech.glide.load.model.LazyHeaders
+import eu.kanade.tachiyomi.App
+import eu.kanade.tachiyomi.data.cache.ChapterCache
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.network.NetworkHelper
+import eu.kanade.tachiyomi.data.network.get
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.source.model.MangasPage
+import eu.kanade.tachiyomi.data.source.model.Page
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.Jsoup
+import rx.Observable
+import rx.schedulers.Schedulers
+import java.util.*
+import javax.inject.Inject
+
+abstract class Source(context: Context) : BaseSource() {
+
+    @Inject protected lateinit var networkService: NetworkHelper
+    @Inject protected lateinit var chapterCache: ChapterCache
+    @Inject protected lateinit var prefs: PreferencesHelper
+
+    val requestHeaders by lazy { headersBuilder().build() }
+
+    val glideHeaders by lazy { glideHeadersBuilder().build() }
+
+    init {
+        App.get(context).component.inject(this)
+    }
+
+    override fun isLoginRequired(): Boolean {
+        return false
+    }
+
+    protected fun popularMangaRequest(page: MangasPage): Request {
+        if (page.page == 1) {
+            page.url = initialPopularMangasUrl
+        }
+
+        return get(page.url, requestHeaders)
+    }
+
+    protected open fun searchMangaRequest(page: MangasPage, query: String): Request {
+        if (page.page == 1) {
+            page.url = getInitialSearchUrl(query)
+        }
+
+        return get(page.url, requestHeaders)
+    }
+
+    protected open fun mangaDetailsRequest(mangaUrl: String): Request {
+        return get(baseUrl + mangaUrl, requestHeaders)
+    }
+
+    protected fun chapterListRequest(mangaUrl: String): Request {
+        return get(baseUrl + mangaUrl, requestHeaders)
+    }
+
+    protected open fun pageListRequest(chapterUrl: String): Request {
+        return get(baseUrl + chapterUrl, requestHeaders)
+    }
+
+    protected open fun imageUrlRequest(page: Page): Request {
+        return get(page.url, requestHeaders)
+    }
+
+    protected open fun imageRequest(page: Page): Request {
+        return get(page.imageUrl, requestHeaders)
+    }
+
+    // Get the most popular mangas from the source
+    fun pullPopularMangasFromNetwork(page: MangasPage): Observable<MangasPage> {
+        return networkService.requestBody(popularMangaRequest(page), true)
+                .map { Jsoup.parse(it) }
+                .doOnNext { doc -> page.mangas = parsePopularMangasFromHtml(doc) }
+                .doOnNext { doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page) }
+                .map { response -> page }
+    }
+
+    // Get mangas from the source with a query
+    fun searchMangasFromNetwork(page: MangasPage, query: String): Observable<MangasPage> {
+        return networkService.requestBody(searchMangaRequest(page, query), true)
+                .map { Jsoup.parse(it) }
+                .doOnNext { doc -> page.mangas = parseSearchFromHtml(doc) }
+                .doOnNext { doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query) }
+                .map { response -> page }
+    }
+
+    // Get manga details from the source
+    fun pullMangaFromNetwork(mangaUrl: String): Observable<Manga> {
+        return networkService.requestBody(mangaDetailsRequest(mangaUrl))
+                .flatMap { Observable.just(parseHtmlToManga(mangaUrl, it)) }
+    }
+
+    // Get chapter list of a manga from the source
+    open fun pullChaptersFromNetwork(mangaUrl: String): Observable<List<Chapter>> {
+        return networkService.requestBody(chapterListRequest(mangaUrl))
+                .flatMap { unparsedHtml ->
+                    val chapters = parseHtmlToChapters(unparsedHtml)
+                    if (!chapters.isEmpty())
+                        Observable.just(chapters)
+                    else
+                        Observable.error(Exception("No chapters found"))
+                }
+    }
+
+    fun getCachedPageListOrPullFromNetwork(chapterUrl: String): Observable<List<Page>> {
+        return chapterCache.getPageListFromCache(getChapterCacheKey(chapterUrl))
+                .onErrorResumeNext { pullPageListFromNetwork(chapterUrl) }
+                .onBackpressureBuffer()
+    }
+
+    fun pullPageListFromNetwork(chapterUrl: String): Observable<List<Page>> {
+        return networkService.requestBody(pageListRequest(chapterUrl))
+                .flatMap { unparsedHtml ->
+                    val pages = convertToPages(parseHtmlToPageUrls(unparsedHtml))
+                    if (!pages.isEmpty())
+                        Observable.just(parseFirstPage(pages, unparsedHtml))
+                    else
+                        Observable.error(Exception("Page list is empty"))
+                }
+    }
+
+    fun getAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
+        return Observable.from(pages)
+                .filter { page -> page.imageUrl != null }
+                .mergeWith(getRemainingImageUrlsFromPageList(pages))
+    }
+
+    // Get the URLs of the images of a chapter
+    fun getRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
+        return Observable.from(pages)
+                .filter { page -> page.imageUrl == null }
+                .concatMap { getImageUrlFromPage(it) }
+    }
+
+    fun getImageUrlFromPage(page: Page): Observable<Page> {
+        page.status = Page.LOAD_PAGE
+        return networkService.requestBody(imageUrlRequest(page))
+                .flatMap { unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)) }
+                .onErrorResumeNext { e ->
+                    page.status = Page.ERROR
+                    Observable.just<String>(null)
+                }
+                .flatMap { imageUrl ->
+                    page.imageUrl = imageUrl
+                    Observable.just(page)
+                }
+                .subscribeOn(Schedulers.io())
+    }
+
+    fun getCachedImage(page: Page): Observable<Page> {
+        val pageObservable = Observable.just(page)
+        if (page.imageUrl == null)
+            return pageObservable
+
+        return pageObservable
+                .flatMap { p ->
+                    if (!chapterCache.isImageInCache(page.imageUrl)) {
+                        return@flatMap cacheImage(page)
+                    }
+                    Observable.just(page)
+                }
+                .flatMap { p ->
+                    page.imagePath = chapterCache.getImagePath(page.imageUrl)
+                    page.status = Page.READY
+                    Observable.just(page)
+                }
+                .onErrorResumeNext { e ->
+                    page.status = Page.ERROR
+                    Observable.just(page)
+                }
+    }
+
+    private fun cacheImage(page: Page): Observable<Page> {
+        page.status = Page.DOWNLOAD_IMAGE
+        return getImageProgressResponse(page)
+                .flatMap { resp ->
+                    chapterCache.putImageToCache(page.imageUrl, resp)
+                    Observable.just(page)
+                }
+    }
+
+    fun getImageProgressResponse(page: Page): Observable<Response> {
+        return networkService.requestBodyProgress(imageRequest(page), page)
+    }
+
+    fun savePageList(chapterUrl: String, pages: List<Page>?) {
+        if (pages != null)
+            chapterCache.putPageListToCache(getChapterCacheKey(chapterUrl), pages)
+    }
+
+    protected fun convertToPages(pageUrls: List<String>): List<Page> {
+        val pages = ArrayList<Page>()
+        for (i in pageUrls.indices) {
+            pages.add(Page(i, pageUrls[i]))
+        }
+        return pages
+    }
+
+    protected open fun parseFirstPage(pages: List<Page>, unparsedHtml: String): List<Page> {
+        val firstImage = parseHtmlToImageUrl(unparsedHtml)
+        pages[0].imageUrl = firstImage
+        return pages
+    }
+
+    protected fun getChapterCacheKey(chapterUrl: String): String {
+        return "$id$chapterUrl"
+    }
+
+    // Overridable method to allow custom parsing.
+    open fun parseChapterNumber(chapter: Chapter) {
+
+    }
+
+    protected fun glideHeadersBuilder(): LazyHeaders.Builder {
+        val builder = LazyHeaders.Builder()
+        for ((key, value) in requestHeaders.toMultimap()) {
+            builder.addHeader(key, value[0])
+        }
+
+        return builder
+    }
+
+}

+ 25 - 15
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.java

@@ -39,6 +39,7 @@ import okhttp3.Headers;
 import okhttp3.Request;
 import okhttp3.Response;
 import rx.Observable;
+import rx.functions.Func1;
 
 public class Batoto extends LoginSource {
 
@@ -106,13 +107,13 @@ public class Batoto extends LoginSource {
     @Override
     protected Request mangaDetailsRequest(String mangaUrl) {
         String mangaId = mangaUrl.substring(mangaUrl.lastIndexOf("r") + 1);
-        return ReqKt.get(String.format(MANGA_URL, mangaId), requestHeaders);
+        return ReqKt.get(String.format(MANGA_URL, mangaId), getRequestHeaders());
     }
 
     @Override
     protected Request pageListRequest(String pageUrl) {
         String id = pageUrl.substring(pageUrl.indexOf("#") + 1);
-        return ReqKt.get(String.format(CHAPTER_URL, id), requestHeaders);
+        return ReqKt.get(String.format(CHAPTER_URL, id), getRequestHeaders());
     }
 
     @Override
@@ -121,7 +122,7 @@ public class Batoto extends LoginSource {
         int start = pageUrl.indexOf("#") + 1;
         int end = pageUrl.indexOf("_", start);
         String id = pageUrl.substring(start, end);
-        return ReqKt.get(String.format(PAGE_URL, id, pageUrl.substring(end+1)), requestHeaders);
+        return ReqKt.get(String.format(PAGE_URL, id, pageUrl.substring(end+1)), getRequestHeaders());
     }
 
     private List<Manga> parseMangasFromHtml(Document parsedHtml) {
@@ -293,7 +294,7 @@ public class Batoto extends LoginSource {
     }
 
     @Override
-    protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
+    protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
         if (!unparsedHtml.contains("Want to see this chapter per page instead?")) {
             String firstImage = parseHtmlToImageUrl(unparsedHtml);
             pages.get(0).setImageUrl(firstImage);
@@ -305,7 +306,7 @@ public class Batoto extends LoginSource {
                 pages.get(i).setImageUrl(imageUrls.get(i).attr("src"));
             }
         }
-        return pages;
+        return (List<Page>) pages;
     }
 
     @Override
@@ -320,10 +321,16 @@ public class Batoto extends LoginSource {
     }
 
     @Override
-    public Observable<Boolean> login(String username, String password) {
-        return networkService.requestBody(ReqKt.get(LOGIN_URL, requestHeaders))
-                .flatMap(response -> doLogin(response, username, password))
-                .map(this::isAuthenticationSuccessful);
+    public Observable<Boolean> login(final String username, final String password) {
+        return getNetworkService().requestBody(ReqKt.get(LOGIN_URL, getRequestHeaders()))
+                .flatMap(new Func1<String, Observable<Response>>() {
+                    @Override
+                    public Observable<Response> call(String response) {return doLogin(response, username, password);}
+                })
+                .map(new Func1<Response, Boolean>() {
+                    @Override
+                    public Boolean call(Response resp) {return isAuthenticationSuccessful(resp);}
+                });
     }
 
     private Observable<Response> doLogin(String response, String username, String password) {
@@ -340,7 +347,7 @@ public class Batoto extends LoginSource {
         formBody.add("invisible", "1");
         formBody.add("rememberMe", "1");
 
-        return networkService.request(ReqKt.post(postUrl, requestHeaders, formBody.build()));
+        return getNetworkService().request(ReqKt.post(postUrl, getRequestHeaders(), formBody.build()));
     }
 
     @Override
@@ -351,7 +358,7 @@ public class Batoto extends LoginSource {
     @Override
     public boolean isLogged() {
         try {
-            for ( HttpCookie cookie : networkService.getCookies().get(new URI(BASE_URL)) ) {
+            for ( HttpCookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL)) ) {
                 if (cookie.getName().equals("pass_hash"))
                     return true;
             }
@@ -363,16 +370,19 @@ public class Batoto extends LoginSource {
     }
 
     @Override
-    public Observable<List<Chapter>> pullChaptersFromNetwork(String mangaUrl) {
+    public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
         Observable<List<Chapter>> observable;
-        String username = prefs.getSourceUsername(this);
-        String password = prefs.getSourcePassword(this);
+        String username = getPrefs().getSourceUsername(this);
+        String password = getPrefs().getSourcePassword(this);
         if (username.isEmpty() && password.isEmpty()) {
             observable = Observable.error(new Exception("User not logged"));
         }
         else if (!isLogged()) {
             observable = login(username, password)
-                    .flatMap(result -> super.pullChaptersFromNetwork(mangaUrl));
+                    .flatMap(new Func1<Boolean, Observable<? extends List<Chapter>>>() {
+                        @Override
+                        public Observable<? extends List<Chapter>> call(Boolean result) {return Batoto.super.pullChaptersFromNetwork(mangaUrl);}
+                    });
         }
         else {
             observable = super.pullChaptersFromNetwork(mangaUrl);

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.java

@@ -84,12 +84,12 @@ public class Kissmanga extends Source {
         form.add("status", "");
         form.add("genres", "");
 
-        return ReqKt.post(page.url, requestHeaders, form.build());
+        return ReqKt.post(page.url, getRequestHeaders(), form.build());
     }
 
     @Override
     protected Request pageListRequest(String chapterUrl) {
-        return ReqKt.post(getBaseUrl() + chapterUrl, requestHeaders);
+        return ReqKt.post(getBaseUrl() + chapterUrl, getRequestHeaders());
     }
 
     @Override
@@ -215,7 +215,7 @@ public class Kissmanga extends Source {
     }
 
     @Override
-    protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
+    protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
         Pattern p = Pattern.compile("lstImages.push\\(\"(.+?)\"");
         Matcher m = p.matcher(unparsedHtml);
 
@@ -223,7 +223,7 @@ public class Kissmanga extends Source {
         while (m.find()) {
             pages.get(i++).setImageUrl(m.group(1));
         }
-        return pages;
+        return (List<Page>) pages;
     }
 
     @Override

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.java

@@ -218,7 +218,7 @@ public class Mangachan extends Source {
     }
 
     @Override
-    protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
+    protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
         int beginIndex = unparsedHtml.indexOf("fullimg\":[");
         int endIndex = unparsedHtml.indexOf("]", beginIndex);
 
@@ -230,7 +230,7 @@ public class Mangachan extends Source {
             pages.get(i).setImageUrl(pageUrls[i].replaceAll("im.?\\.", ""));
         }
 
-        return pages;
+        return (List<Page>) pages;
     }
 
     @Override

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.java

@@ -203,7 +203,7 @@ public class Mintmanga extends Source {
     }
 
     @Override
-    protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
+    protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
         int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
         int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
 
@@ -215,7 +215,7 @@ public class Mintmanga extends Source {
             String page = urlParts[1] + urlParts[0] + urlParts[2];
             pages.get(i).setImageUrl(page);
         }
-        return pages;
+        return (List<Page>) pages;
     }
 
     @Override

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.java

@@ -203,7 +203,7 @@ public class Readmanga extends Source {
     }
 
     @Override
-    protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
+    protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
         int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
         int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
 
@@ -215,7 +215,7 @@ public class Readmanga extends Source {
             String page = urlParts[1] + urlParts[0] + urlParts[2];
             pages.get(i).setImageUrl(page);
         }
-        return pages;
+        return (List<Page>) pages;
     }
 
     @Override

+ 0 - 62
app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java

@@ -1,62 +0,0 @@
-package eu.kanade.tachiyomi.injection.component;
-
-import android.app.Application;
-
-import javax.inject.Singleton;
-
-import dagger.Component;
-import eu.kanade.tachiyomi.data.download.DownloadService;
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService;
-import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService;
-import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
-import eu.kanade.tachiyomi.data.source.base.Source;
-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.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.manga.MangaActivity;
-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.RecentChaptersPresenter;
-import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
-
-@Singleton
-@Component(
-        modules = {
-                AppModule.class,
-                DataModule.class
-        }
-)
-public interface AppComponent {
-
-    void inject(LibraryPresenter libraryPresenter);
-    void inject(MangaPresenter mangaPresenter);
-    void inject(CataloguePresenter cataloguePresenter);
-    void inject(MangaInfoPresenter mangaInfoPresenter);
-    void inject(ChaptersPresenter chaptersPresenter);
-    void inject(ReaderPresenter readerPresenter);
-    void inject(DownloadPresenter downloadPresenter);
-    void inject(MyAnimeListPresenter myAnimeListPresenter);
-    void inject(CategoryPresenter categoryPresenter);
-    void inject(RecentChaptersPresenter recentChaptersPresenter);
-
-    void inject(MangaActivity mangaActivity);
-    void inject(SettingsActivity settingsActivity);
-
-    void inject(Source source);
-    void inject(MangaSyncService mangaSyncService);
-
-    void inject(LibraryUpdateService libraryUpdateService);
-    void inject(DownloadService downloadService);
-    void inject(UpdateMangaSyncService updateMangaSyncService);
-
-    void inject(UpdateDownloader updateDownloader);
-    Application application();
-
-}

+ 55 - 0
app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.kt

@@ -0,0 +1,55 @@
+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.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
+import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
+import eu.kanade.tachiyomi.data.source.base.Source
+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.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.manga.MangaActivity
+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.RecentChaptersPresenter
+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(mangaActivity: MangaActivity)
+    fun inject(settingsActivity: SettingsActivity)
+
+    fun inject(source: Source)
+    fun inject(mangaSyncService: MangaSyncService)
+
+    fun inject(libraryUpdateService: LibraryUpdateService)
+    fun inject(downloadService: DownloadService)
+    fun inject(updateMangaSyncService: UpdateMangaSyncService)
+
+    fun inject(updateDownloader: UpdateDownloader)
+    fun application(): Application
+
+}

+ 0 - 28
app/src/main/java/eu/kanade/tachiyomi/injection/module/AppModule.java

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

+ 21 - 0
app/src/main/java/eu/kanade/tachiyomi/injection/module/AppModule.kt

@@ -0,0 +1,21 @@
+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
+    }
+
+}

+ 0 - 73
app/src/main/java/eu/kanade/tachiyomi/injection/module/DataModule.java

@@ -1,73 +0,0 @@
-package eu.kanade.tachiyomi.injection.module;
-
-import android.app.Application;
-
-import javax.inject.Singleton;
-
-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;
-
-/**
- * Provide dependencies to the DataManager, mainly Helper classes and Retrofit services.
- */
-@Module
-public class DataModule {
-
-    @Provides
-    @Singleton
-    PreferencesHelper providePreferencesHelper(Application app) {
-        return new PreferencesHelper(app);
-    }
-
-    @Provides
-    @Singleton
-    DatabaseHelper provideDatabaseHelper(Application app) {
-        return new DatabaseHelper(app);
-    }
-
-    @Provides
-    @Singleton
-    ChapterCache provideChapterCache(Application app) {
-        return new ChapterCache(app);
-    }
-
-    @Provides
-    @Singleton
-    CoverCache provideCoverCache(Application app) {
-        return new CoverCache(app);
-    }
-
-    @Provides
-    @Singleton
-    NetworkHelper provideNetworkHelper(Application app) {
-        return new NetworkHelper(app);
-    }
-
-    @Provides
-    @Singleton
-    SourceManager provideSourceManager(Application app) {
-        return new SourceManager(app);
-    }
-
-    @Provides
-    @Singleton
-    DownloadManager provideDownloadManager(
-            Application app, SourceManager sourceManager, PreferencesHelper preferences) {
-        return new DownloadManager(app, sourceManager, preferences);
-    }
-
-    @Provides
-    @Singleton
-    MangaSyncManager provideMangaSyncManager(Application app) {
-        return new MangaSyncManager(app);
-    }
-
-}

+ 70 - 0
app/src/main/java/eu/kanade/tachiyomi/injection/module/DataModule.kt

@@ -0,0 +1,70 @@
+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)
+    }
+
+}

+ 9 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.java

@@ -58,12 +58,15 @@ public abstract class BaseRxActivity<P extends Presenter> extends BaseActivity i
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         final PresenterFactory<P> superFactory = getPresenterFactory();
-        setPresenterFactory(() -> {
-            P presenter = superFactory.createPresenter();
-            App app = (App) getApplication();
-            app.getComponentReflection().inject(presenter);
-            ((BasePresenter)presenter).setContext(app.getApplicationContext());
-            return presenter;
+        setPresenterFactory(new PresenterFactory<P>() {
+            @Override
+            public P createPresenter() {
+                P presenter = superFactory.createPresenter();
+                App app = (App) BaseRxActivity.this.getApplication();
+                app.getComponentReflection().inject(presenter);
+                ((BasePresenter) presenter).setContext(app.getApplicationContext());
+                return presenter;
+            }
         });
 
         super.onCreate(savedInstanceState);

+ 9 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.java

@@ -56,12 +56,15 @@ public abstract class BaseRxFragment<P extends Presenter> extends BaseFragment i
     @Override
     public void onCreate(Bundle bundle) {
         final PresenterFactory<P> superFactory = getPresenterFactory();
-        setPresenterFactory(() -> {
-            P presenter = superFactory.createPresenter();
-            App app = (App) getActivity().getApplication();
-            app.getComponentReflection().inject(presenter);
-            ((BasePresenter)presenter).setContext(app.getApplicationContext());
-            return presenter;
+        setPresenterFactory(new PresenterFactory<P>() {
+            @Override
+            public P createPresenter() {
+                P presenter = superFactory.createPresenter();
+                App app = (App) BaseRxFragment.this.getActivity().getApplication();
+                app.getComponentReflection().inject(presenter);
+                ((BasePresenter) presenter).setContext(app.getApplicationContext());
+                return presenter;
+            }
         });
 
         super.onCreate(bundle);

+ 12 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/RxPresenter.java

@@ -220,7 +220,10 @@ public class RxPresenter<View> extends Presenter<View> {
      * @param observableFactory a factory that should return an Observable when the startable should run.
      */
     public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory) {
-        restartables.put(startableId, () -> observableFactory.call().subscribe());
+        restartables.put(startableId, new Func0<Subscription>() {
+            @Override
+            public Subscription call() {return observableFactory.call().subscribe();}
+        });
     }
 
     /**
@@ -234,7 +237,10 @@ public class RxPresenter<View> extends Presenter<View> {
     public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory,
         final Action1<T> onNext, final Action1<Throwable> onError) {
 
-        restartables.put(startableId, () -> observableFactory.call().subscribe(onNext, onError));
+        restartables.put(startableId, new Func0<Subscription>() {
+            @Override
+            public Subscription call() {return observableFactory.call().subscribe(onNext, onError);}
+        });
     }
 
     /**
@@ -245,7 +251,10 @@ public class RxPresenter<View> extends Presenter<View> {
      * @param onNext            a callback that will be called when received data should be delivered to view.
      */
     public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory, final Action1<T> onNext) {
-        restartables.put(startableId, () -> observableFactory.call().subscribe(onNext));
+        restartables.put(startableId, new Func0<Subscription>() {
+            @Override
+            public Subscription call() {return observableFactory.call().subscribe(onNext);}
+        });
     }
     
     /**

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt

@@ -211,7 +211,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         val obs = if (query.isNullOrEmpty())
             source.pullPopularMangasFromNetwork(nextMangasPage)
         else
-            source.searchMangasFromNetwork(nextMangasPage, query)
+            source.searchMangasFromNetwork(nextMangasPage, query!!)
 
         return obs.subscribeOn(Schedulers.io())
                 .doOnNext { lastMangasPage = it }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt

@@ -37,7 +37,7 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
         // Get categories as list
         restartableLatestCache(GET_CATEGORIES,
                 {
-                    db.categories.asRxObservable()
+                    db.getCategories().asRxObservable()
                             .doOnNext { categories -> this.categories = categories }
                             .observeOn(AndroidSchedulers.mainThread())
                 }, CategoryActivity::setCategories)
@@ -76,7 +76,7 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
      *
      * @param categories list of categories
      */
-    fun deleteCategories(categories: List<Category?>?) {
+    fun deleteCategories(categories: List<Category>) {
         db.deleteCategories(categories).asRxObservable().subscribe()
     }
 

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -127,7 +127,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
      * @return an observable of the categories.
      */
     fun getCategoriesObservable(): Observable<List<Category>> {
-        return db.categories.asRxObservable()
+        return db.getCategories().asRxObservable()
                 .doOnNext { categories -> this.categories = categories }
     }
 
@@ -138,7 +138,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
      * value.
      */
     fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> {
-        return db.libraryMangas.asRxObservable()
+        return db.getLibraryMangas().asRxObservable()
                 .flatMap { mangas ->
                     Observable.from(mangas)
                             .filter {

+ 33 - 23
app/src/main/java/eu/kanade/tachiyomi/util/DynamicConcurrentMergeOperator.java

@@ -10,6 +10,8 @@ import rx.Observable;
 import rx.Observable.Operator;
 import rx.Subscriber;
 import rx.Subscription;
+import rx.functions.Action0;
+import rx.functions.Action1;
 import rx.functions.Func1;
 import rx.subscriptions.CompositeSubscription;
 import rx.subscriptions.Subscriptions;
@@ -58,29 +60,35 @@ public class DynamicConcurrentMergeOperator<T, R> implements Operator<R, T> {
         }
 
         public void init(Observable<Integer> workerCount) {
-            Subscription wc = workerCount.subscribe(n -> {
-                int n0 = workers.size();
-                if (n0 < n) {
-                    for (int i = n0; i < n; i++) {
-                        DynamicWorker<T, R> dw = new DynamicWorker<>(++id, this);
-                        workers.add(dw);
-                        request(1);
-                        dw.tryNext();
-                    }
-                } else if (n0 > n) {
-                    for (int i = 0; i < n; i++) {
-                        workers.get(i).start();
-                    }
+            Subscription wc = workerCount.subscribe(new Action1<Integer>() {
+                @Override
+                public void call(Integer n) {
+                    int n0 = workers.size();
+                    if (n0 < n) {
+                        for (int i = n0; i < n; i++) {
+                            DynamicWorker<T, R> dw = new DynamicWorker<>(++id, DynamicConcurrentMerge.this);
+                            workers.add(dw);
+                            DynamicConcurrentMerge.this.request(1);
+                            dw.tryNext();
+                        }
+                    } else if (n0 > n) {
+                        for (int i = 0; i < n; i++) {
+                            workers.get(i).start();
+                        }
 
-                    for (int i = n0 - 1; i >= n; i--) {
-                        workers.get(i).stop();
+                        for (int i = n0 - 1; i >= n; i--) {
+                            workers.get(i).stop();
+                        }
                     }
-                }
 
-                if (!once.get() && once.compareAndSet(false, true)) {
-                    request(n);
+                    if (!once.get() && once.compareAndSet(false, true)) {
+                        DynamicConcurrentMerge.this.request(n);
+                    }
                 }
-            }, this::onError);
+            }, new Action1<Throwable>() {
+                @Override
+                public void call(Throwable e) {DynamicConcurrentMerge.this.onError(e);}
+            });
 
             composite.add(wc);
         }
@@ -138,9 +146,9 @@ public class DynamicConcurrentMergeOperator<T, R> implements Operator<R, T> {
                     return;
                 }
 
-                Observable out = parent.mapper.call(t);
+                Observable<? extends R> out = parent.mapper.call(t);
 
-                Subscriber<R> s = new Subscriber<R>() {
+                final Subscriber<R> s = new Subscriber<R>() {
                     @Override
                     public void onNext(R t) {
                         parent.actual.onNext(t);
@@ -163,9 +171,11 @@ public class DynamicConcurrentMergeOperator<T, R> implements Operator<R, T> {
                 };
 
                 parent.composite.add(s);
-                s.add(Subscriptions.create(() -> parent.composite.remove(s)));
+                s.add(Subscriptions.create(new Action0() {
+                    @Override
+                    public void call() {parent.composite.remove(s);}
+                }));
 
-                // Unchecked assignment to avoid weird Android Studio errors
                 out.subscribe(s);
             }
         }

+ 0 - 26
app/src/main/java/eu/kanade/tachiyomi/util/RxPager.java

@@ -1,26 +0,0 @@
-package eu.kanade.tachiyomi.util;
-
-import android.util.Pair;
-
-import java.util.List;
-
-import rx.Observable;
-import rx.functions.Func1;
-import rx.subjects.PublishSubject;
-
-public class RxPager<T> {
-
-    private final PublishSubject<List<T>> results = PublishSubject.create();
-    private int requestedCount;
-
-    public Observable<Pair<Integer, List<T>>> results() {
-        requestedCount = 0;
-        return results.map(list -> Pair.create(requestedCount++, list));
-    }
-
-    public Observable<List<T>> request(Func1<Integer, Observable<List<T>>> networkObservable) {
-        return networkObservable.call(requestedCount).doOnNext(results::onNext);
-    }
-
-}
-

+ 21 - 0
app/src/main/java/eu/kanade/tachiyomi/util/RxPager.kt

@@ -0,0 +1,21 @@
+package eu.kanade.tachiyomi.util
+
+import android.util.Pair
+import rx.Observable
+import rx.subjects.PublishSubject
+
+class RxPager<T> {
+
+    private val results = PublishSubject.create<List<T>>()
+    private var requestedCount: Int = 0
+
+    fun results(): Observable<Pair<Int, List<T>>> {
+        requestedCount = 0
+        return results.map { Pair(requestedCount++, it) }
+    }
+
+    fun request(networkObservable: (Int) -> Observable<List<T>>) =
+        networkObservable(requestedCount).doOnNext { results.onNext(it) }
+
+}
+

+ 0 - 1
build.gradle

@@ -8,7 +8,6 @@ buildscript {
     dependencies {
         classpath 'com.android.tools.build:gradle:2.1.0-alpha3'
         classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
-        classpath 'me.tatarka:gradle-retrolambda:3.2.4'
         classpath 'com.github.ben-manes:gradle-versions-plugin:0.12.0'
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files