Compare commits

...

66 Commits

Author SHA1 Message Date
len
74e3d387eb Release v0.2.3 2016-07-24 15:41:58 +02:00
len
8f83f497d5 Update history custom put resolver 2016-07-23 15:41:47 +02:00
len
6999fa858e Fix #400 2016-07-23 12:09:06 +02:00
len
8c1bedf796 Back button now returns to start screen. Also fix #356 2016-07-20 19:09:28 +02:00
len
1090c04fe3 Remove deprecated calls and fix a potential race condition 2016-07-18 21:01:51 +02:00
33b04427d5 Added a startup screen preference option (#395)
* Added a startup screen preference option

* changed string and keys to be consistent
2016-07-18 19:58:18 +02:00
len
f7bb356abd Fix exception thrown when Batoto search is empty 2016-07-16 17:25:22 +02:00
len
e16bf0698e Minor fix 2016-07-15 18:22:24 +02:00
len
e6190683dd Observable calls can now be retried, previously all retries were failing 2016-07-10 12:14:30 +02:00
len
e08e41ae0d Remove most unused settings from the reader (keep screen on and page transitions), they are still available in the app's settings. Also lower minimum brightness to -75% 2016-07-08 22:31:46 +02:00
len
5f1a89df63 Remove newThread usages, it probably fixes random crashes 2016-07-08 18:23:03 +02:00
len
f15df40a54 Add an overlay on top of the reader to simulate a lower brightness. Closes #362 2016-07-07 23:18:22 +02:00
len
a32e0e4ec5 Fix #361 2016-07-04 00:27:45 +02:00
len
3e8ac6b2d0 Fix for #361? 2016-07-03 21:48:55 +02:00
len
50a773f456 Fix YAML parser crashing the app on Kitkat and lower 2016-07-03 21:33:07 +02:00
len
42484d718a And a few more crashes fixed in preferences 2016-07-03 21:19:34 +02:00
len
81887000a8 Fix a few crashes 2016-07-03 21:04:09 +02:00
len
987473df44 Minor changes 2016-07-03 18:49:02 +02:00
len
3680eb0bf5 Recently read improvements: Open next chapter if read, local date formatting 2016-07-03 17:58:39 +02:00
len
3dbdc495e7 Minor changes 2016-07-03 14:25:51 +02:00
466515c801 Implement "Wie Manga!" (#379)
* Implement Wie Manga!

* Unnecessary import
2016-07-02 22:16:20 +02:00
len
e198f7e671 Add icons for settings 2016-07-02 22:14:04 +02:00
len
5fe1799dab Fix #333 2016-07-02 14:12:52 +02:00
len
ce7118084a Downloads view now uses a copy of the original queue. Fixes #351 and some crashes while scrolling and removing a download from the queue 2016-07-01 18:30:46 +02:00
len
06786322ca Bump dependencies 2016-07-01 01:52:05 +02:00
len
130b7501d1 Remove no predictive animations. Upgrade Kotlin to 1.0.3 2016-07-01 01:39:57 +02:00
len
864f001c3e Add portuguese translation by @MrAmnesiac 2016-06-30 16:10:30 +02:00
len
1553ce973f Ignore the first spinner selection 2016-06-30 13:02:14 +02:00
72811e59f5 Spanish UI translation (#365)
Added spanish translation
2016-06-29 15:32:05 +02:00
4c1da3575b Cleanup - squid:S1155 - Collection.isEmpty() should be used to test for emptiness (#371) 2016-06-29 15:31:41 +02:00
05c0516a57 New reader menu (#368) 2016-06-27 16:46:31 +02:00
fe6dff9086 Handle a missing page list in MangaHere (#366)
This typically happens when a manga is pulled from their catalog (I tested it on Nisekoi). Previous behavior led to a NullPointerError, now gives an empty page list.

Giving a reason to the user beyond "Empty Page list" would be a good idea in the future (this seems to be one holdup for #220), but there doesn't seem to be an obvious place to put it without touching the base classes.  In the meantime, this is far more informative than null errors.
2016-06-25 13:01:44 +02:00
len
b6df5e6ee6 Reader fixes (MAL not updating in certain scenarios) 2016-06-24 13:39:34 +02:00
3ee5774870 Use Cloudflare client for ReadManga.Today (#363) 2016-06-23 14:05:20 +02:00
c8fbb96f49 Mangasee as image source (#355)
* Mangasee as image source

* revert

* Mangasee source refactoring
2016-06-20 15:37:35 +02:00
len
143303f7df Parser improvements 2016-06-20 00:57:29 +02:00
len
585f7ec17d Remove getAbsoluteUrl method 2016-06-18 17:37:41 +02:00
len
9beeca652f Rewrite preferences with a modified support library v7 2016-06-16 20:52:51 +02:00
len
cd92569355 Restart inject module when the app is created 2016-06-15 17:58:28 +02:00
len
a82e1d0e45 Remove unneeded annotations and some cleanup 2016-06-15 17:47:44 +02:00
len
5ad06df4ac Fix chapters with 1 page not marked as read 2016-06-15 16:47:59 +02:00
len
5cfd5da338 Convert some classes to Kotlin 2016-06-15 16:37:48 +02:00
len
b1d7167112 Bump dependencies 2016-06-15 13:18:27 +02:00
len
a475ecec4d Test package in Kotlin 2016-06-15 12:53:12 +02:00
5c98e020f4 Merge pull request #350 from inorichi/dev
Rewrite DB models, tests and add a chapter loader.
2016-06-15 12:31:42 +02:00
len
eed295587d Fix tests 2016-06-14 15:17:44 +02:00
len
237af4b07d Fix dependency injection and use custom models extending DB ones 2016-06-14 15:17:37 +02:00
len
658860fdff Add chapter loader, drop non seamless mode 2016-06-14 15:15:31 +02:00
len
21ba371a32 Replace Dagger2 with Injekt, reorganize dependencies 2016-06-14 15:13:48 +02:00
len
589160242e Rewrite database models in Kotlin 2016-06-14 15:11:23 +02:00
4de8b6e9a8 Update Mangachan address and fix loading covers after update (#347) 2016-06-11 15:59:04 +02:00
len
e79d536f33 Update readme 2016-06-10 20:48:43 +02:00
len
9e90096328 Match release version 2016-06-10 20:31:33 +02:00
f0a382c21a Improve regex for pages from Readmanga and Mintmanga (#345) 2016-06-09 19:48:23 +02:00
len
682a2c7546 Delete file when exception is thrown 2016-06-09 15:46:08 +02:00
len
2d1e85f280 Fix scroll position with many categories. Closes #332 2016-06-09 14:27:11 +02:00
len
dbec4fc15e Cloudflare fix. Closes #344 2016-06-09 11:32:24 +02:00
95cd77e749 Multiple quality improvements - squid:S1213, squid:S1943, squid:S1066 (#342) 2016-06-08 08:44:12 +02:00
1f8126e2af Use cardBackgroundColor instead of android:Background (#339) 2016-06-07 20:59:43 +02:00
86db7497e9 Small card fixes (#338) 2016-06-07 20:36:40 +02:00
172305fc6a Wrong card background fix + bump gradle version (#337) 2016-06-07 20:14:36 +02:00
dad9dcd742 Improve getAbsolutUrl method (#336)
Also fix Mangachan most popular pages
2016-06-07 20:04:50 +02:00
len
59b90a94d0 Remove covers on error. #334 2016-06-06 20:45:22 +02:00
len
93fc5944f3 Remove unneeded casts 2016-06-06 16:53:58 +02:00
len
7039216eae Manual mappings. Code generation on java classes (better compilation times) 2016-06-06 16:27:24 +02:00
7ba898f701 Added recently read tab (#316) 2016-06-06 15:26:56 +02:00
222 changed files with 7146 additions and 4928 deletions

View File

@ -1,6 +1,6 @@
| Build | Download | Auto Update | | Build | Download | Auto Update |
|-------|----------|-------------| |-------|----------|-------------|
| [![TeamCity (simple build status)](https://img.shields.io/teamcity/https/teamcity.kanade.eu/s/tachiyomi_Build.svg)](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=2592000&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [![fdroid release](https://img.shields.io/badge/stable-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid debug](https://img.shields.io/badge/dev-F--Droid-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) | | [![TeamCity (simple build status)](https://img.shields.io/teamcity/https/teamcity.kanade.eu/s/tachiyomi_Build.svg)](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [![fdroid release](https://img.shields.io/badge/stable-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid debug](https://img.shields.io/badge/dev-F--Droid-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md) ## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)

View File

@ -38,9 +38,8 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 23 targetSdkVersion 23
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 8 versionCode 10
versionCode project.findProperty('versionCode')?.toInteger() ?: 8 versionName "0.2.3"
versionName "0.2.2"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -52,7 +51,7 @@ android {
buildTypes { buildTypes {
debug { debug {
versionNameSuffix ".${getCommitCount()}" versionNameSuffix "-${getCommitCount()}"
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
} }
release { release {
@ -79,59 +78,47 @@ android {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
// http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric
useLibrary 'org.apache.http.legacy'
}
kapt {
generateStubs = true
} }
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:customtabs:$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.1'
compile 'io.reactivex:rxjava:1.1.5' compile 'io.reactivex:rxjava:1.1.6'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1' compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
// Network client // Network client
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.1.0'
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.7'
compile 'com.github.salomonbrys.kotson:kotson:2.3.0'
// YAML // YAML
compile 'org.yaml:snakeyaml:1.17' compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
// JavaScript engine // JavaScript engine
compile 'com.squareup.duktape:duktape-android:0.9.5' compile 'com.squareup.duktape:duktape-android:0.9.5'
@ -146,19 +133,16 @@ 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" compile "com.pushtorefresh.storio:sqlite:1.9.0"
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$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'
@ -168,30 +152,30 @@ dependencies {
compile 'com.jakewharton.timber:timber:4.1.2' compile 'com.jakewharton.timber:timber:4.1.2'
// Crash reports // Crash reports
compile 'ch.acra:acra:4.8.5' compile 'ch.acra:acra:4.9.0'
// UI // UI
compile 'com.dmitrymalkovich.android:material-design-dimens:1.2'
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
compile 'eu.davidea:flexible-adapter:4.2.0' compile 'eu.davidea:flexible-adapter:4.2.0'
compile 'com.nononsenseapps:filepicker:2.5.2' compile 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e' compile 'com.github.amulyakhare:TextDrawable:558677e'
compile 'com.afollestad.material-dialogs:core:0.8.5.9' compile 'com.afollestad.material-dialogs:core:0.8.6.1'
compile 'net.xpece.android:support-preference:0.8.1'
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
compile 'org.adw.library:discrete-seekbar:1.0.1'
// 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.1'
exclude group: 'commons-logging', module: 'commons-logging'
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"
} }
buildscript { buildscript {
ext.kotlin_version = '1.0.2' ext.kotlin_version = '1.0.3'
repositories { repositories {
mavenCentral() mavenCentral()
} }

View File

@ -1,7 +1,5 @@
-dontobfuscate -dontobfuscate
-keep class eu.kanade.tachiyomi.injection.** { *; }
# OkHttp # OkHttp
-keepattributes Signature -keepattributes Signature
-keepattributes *Annotation* -keepattributes *Annotation*

View File

@ -1,15 +1,12 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.app.Application 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.ACRA
import org.acra.annotation.ReportsCrashes import org.acra.annotation.ReportsCrashes
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar
@ReportsCrashes( @ReportsCrashes(
formUri = "http://tachiyomi.kanade.eu/crash_report", formUri = "http://tachiyomi.kanade.eu/crash_report",
@ -20,43 +17,18 @@ 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
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this))
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
component = prepareAppComponent().build()
componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
setupTheme()
setupAcra() setupAcra()
} }
private fun setupTheme() {
appTheme = PreferencesHelper.getTheme(this)
}
protected open fun prepareAppComponent(): DaggerAppComponent.Builder {
return DaggerAppComponent.builder()
.appModule(AppModule(this))
}
protected open fun setupAcra() { protected open fun setupAcra() {
ACRA.init(this) ACRA.init(this)
} }
companion object {
@JvmStatic
fun get(context: Context): App {
return context.applicationContext as App
}
}
} }

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

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database
import android.content.Context import android.content.Context
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
import eu.kanade.tachiyomi.data.database.mappers.*
import eu.kanade.tachiyomi.data.database.models.* import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.database.queries.* import eu.kanade.tachiyomi.data.database.queries.*
@ -9,15 +10,16 @@ import eu.kanade.tachiyomi.data.database.queries.*
* This class provides operations to manage the database through its interfaces. * This class provides operations to manage the database through its interfaces.
*/ */
open class DatabaseHelper(context: Context) open class DatabaseHelper(context: Context)
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries { : MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
override val db = DefaultStorIOSQLite.builder() override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(DbOpenHelper(context)) .sqliteOpenHelper(DbOpenHelper(context))
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping()) .addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping()) .addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(MangaSync::class.java, MangaSyncSQLiteTypeMapping()) .addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping())
.addTypeMapping(Category::class.java, CategorySQLiteTypeMapping()) .addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping()) .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping())
.build() .build()
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)

View File

@ -3,23 +3,23 @@ package eu.kanade.tachiyomi.data.database
import com.pushtorefresh.storio.sqlite.StorIOSQLite import com.pushtorefresh.storio.sqlite.StorIOSQLite
inline fun StorIOSQLite.inTransaction(block: () -> Unit) { inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
internal().beginTransaction() lowLevel().beginTransaction()
try { try {
block() block()
internal().setTransactionSuccessful() lowLevel().setTransactionSuccessful()
} finally { } finally {
internal().endTransaction() lowLevel().endTransaction()
} }
} }
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T { inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
internal().beginTransaction() lowLevel().beginTransaction()
try { try {
val result = block() val result = block()
internal().setTransactionSuccessful() lowLevel().setTransactionSuccessful()
return result return result
} finally { } finally {
internal().endTransaction() lowLevel().endTransaction()
} }
} }

View File

@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = 2 const val DATABASE_VERSION = 3
} }
override fun onCreate(db: SQLiteDatabase) = with(db) { override fun onCreate(db: SQLiteDatabase) = with(db) {
@ -26,11 +26,13 @@ class DbOpenHelper(context: Context)
execSQL(MangaSyncTable.createTableQuery) execSQL(MangaSyncTable.createTableQuery)
execSQL(CategoryTable.createTableQuery) execSQL(CategoryTable.createTableQuery)
execSQL(MangaCategoryTable.createTableQuery) execSQL(MangaCategoryTable.createTableQuery)
execSQL(HistoryTable.createTableQuery)
// DB indexes // DB indexes
execSQL(MangaTable.createUrlIndexQuery) execSQL(MangaTable.createUrlIndexQuery)
execSQL(MangaTable.createFavoriteIndexQuery) execSQL(MangaTable.createFavoriteIndexQuery)
execSQL(ChapterTable.createMangaIdIndexQuery) execSQL(ChapterTable.createMangaIdIndexQuery)
execSQL(HistoryTable.createChapterIdIndexQuery)
} }
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@ -41,10 +43,15 @@ class DbOpenHelper(context: Context)
db.execSQL("""UPDATE mangas SET thumbnail_url = db.execSQL("""UPDATE mangas SET thumbnail_url =
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""") REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
} }
if (oldVersion < 3) {
// Initialize history tables
db.execSQL(HistoryTable.createTableQuery)
db.execSQL(HistoryTable.createChapterIdIndexQuery)
}
} }
override fun onConfigure(db: SQLiteDatabase) { override fun onConfigure(db: SQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true) db.setForeignKeyConstraintsEnabled(true)
} }
} }

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
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_ID
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
CategoryPutResolver(),
CategoryGetResolver(),
CategoryDeleteResolver()
)
class CategoryPutResolver : DefaultPutResolver<Category>() {
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_NAME, obj.name)
put(COL_ORDER, obj.order)
put(COL_FLAGS, obj.flags)
}
}
class CategoryGetResolver : DefaultGetResolver<Category>() {
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
}
}
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,82 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
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_DATE_FETCH
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_UPLOAD
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
ChapterPutResolver(),
ChapterGetResolver(),
ChapterDeleteResolver()
)
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Chapter) = ContentValues(10).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_URL, obj.url)
put(COL_NAME, obj.name)
put(COL_READ, obj.read)
put(COL_DATE_FETCH, obj.date_fetch)
put(COL_DATE_UPLOAD, obj.date_upload)
put(COL_LAST_PAGE_READ, obj.last_page_read)
put(COL_CHAPTER_NUMBER, obj.chapter_number)
put(COL_SOURCE_ORDER, obj.source_order)
}
}
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
}
}
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
class HistoryTypeMapping : SQLiteTypeMapping<History>(
HistoryPutResolver(),
HistoryGetResolver(),
HistoryDeleteResolver()
)
open class HistoryPutResolver : DefaultPutResolver<History>() {
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_CHAPTER_ID, obj.chapter_id)
put(COL_LAST_READ, obj.last_read)
put(COL_TIME_READ, obj.time_read)
}
}
class HistoryGetResolver : DefaultGetResolver<History>() {
override fun mapFromCursor(cursor: Cursor): History = History().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
}
}
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,59 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_CATEGORY_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
MangaCategoryPutResolver(),
MangaCategoryGetResolver(),
MangaCategoryDeleteResolver()
)
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_CATEGORY_ID, obj.category_id)
}
}
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
}
}
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,78 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
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_LAST_CHAPTER_READ
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_REMOTE_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SCORE
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SYNC_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TOTAL_CHAPTERS
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.TABLE
class MangaSyncTypeMapping : SQLiteTypeMapping<MangaSync>(
MangaSyncPutResolver(),
MangaSyncGetResolver(),
MangaSyncDeleteResolver()
)
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: MangaSync) = ContentValues(9).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_SYNC_ID, obj.sync_id)
put(COL_REMOTE_ID, obj.remote_id)
put(COL_TITLE, obj.title)
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
put(COL_STATUS, obj.status)
put(COL_SCORE, obj.score)
}
}
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
remote_id = cursor.getInt(cursor.getColumnIndex(COL_REMOTE_ID))
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
}
}
class MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() {
override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,96 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
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_AUTHOR
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
MangaPutResolver(),
MangaGetResolver(),
MangaDeleteResolver()
)
class MangaPutResolver : DefaultPutResolver<Manga>() {
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
put(COL_ID, obj.id)
put(COL_SOURCE, obj.source)
put(COL_URL, obj.url)
put(COL_ARTIST, obj.artist)
put(COL_AUTHOR, obj.author)
put(COL_DESCRIPTION, obj.description)
put(COL_GENRE, obj.genre)
put(COL_TITLE, obj.title)
put(COL_STATUS, obj.status)
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
put(COL_FAVORITE, obj.favorite)
put(COL_LAST_UPDATE, obj.last_update)
put(COL_INITIALIZED, obj.initialized)
put(COL_VIEWER, obj.viewer)
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
}
}
open class MangaGetResolver : DefaultGetResolver<Manga>() {
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
source = cursor.getInt(cursor.getColumnIndex(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
}
}
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

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

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

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

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import java.util.*
interface HistoryQueries : DbProvider {
/**
* Insert history into database
* @param history object containing history information
*/
fun insertHistory(history: History) = db.put().`object`(history).prepare()
/**
* Returns history of recent manga containing last read chapter
* @param date recent date range
*/
fun getRecentManga(date: Date) = db.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(RawQuery.builder()
.query(getRecentMangasQuery())
.args(date.time)
.observesTables(HistoryTable.TABLE)
.build())
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java)
.withQuery(RawQuery.builder()
.query(getHistoryByMangaId())
.args(mangaId)
.observesTables(HistoryTable.TABLE)
.build())
.prepare()
/**
* Updates the history last read.
* Inserts history object if not yet in database
* @param history history object
*/
fun updateHistoryLastRead(history: History) = db.put()
.`object`(history)
.withPutResolver(HistoryLastReadPutResolver())
.prepare()
/**
* Updates the history last read.
* Inserts history object if not yet in database
* @param historyList history object list
*/
fun updateHistoryLastRead(historyList: List<History>) = db.put()
.objects(historyList)
.withPutResolver(HistoryLastReadPutResolver())
.prepare()
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database.queries
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
@ -39,6 +40,38 @@ fun getRecentsQuery() = """
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
""" """
/**
* Query to get the recently read chapters of manga from the library up to a date.
* The max_last_read table contains the most recent chapters grouped by manga
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
* and are read after the given time period
* @return return limit is 25
*/
fun getRecentMangasQuery() = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
JOIN ${History.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
JOIN (
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
LIMIT 25
"""
fun getHistoryByMangaId() = """
SELECT ${History.TABLE}.*
FROM ${History.TABLE}
JOIN ${Chapter.TABLE}
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
"""
/** /**
* Query to get the categories for a manga. * Query to get the categories for a manga.

View File

@ -15,7 +15,7 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
val updateQuery = mapToUpdateQuery(chapter) val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter) val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues) val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
} }

View File

@ -15,7 +15,7 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
val updateQuery = mapToUpdateQuery(chapter) val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter) val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues) val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
} }

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import android.support.annotation.NonNull
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
class HistoryLastReadPutResolver : HistoryPutResolver() {
/**
* Updates last_read time of chapter
*/
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(history)
val cursor = db.lowLevel().query(Query.builder()
.table(updateQuery.table())
.where(updateQuery.where())
.whereArgs(updateQuery.whereArgs())
.build())
val putResult: PutResult
try {
if (cursor.count == 0) {
val insertQuery = mapToInsertQuery(history)
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
putResult = PutResult.newInsertResult(insertedId, insertQuery.table())
} else {
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
} finally {
cursor.close()
}
putResult
}
/**
* Creates update query
* @param obj history object
*/
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
.whereArgs(obj.chapter_id)
.build()
/**
* Create content query
* @param history object
*/
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
put(HistoryTable.COL_LAST_READ, history.last_read)
}
}

View File

@ -1,12 +1,11 @@
package eu.kanade.tachiyomi.data.database.resolvers package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor import android.database.Cursor
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() { class LibraryMangaGetResolver : MangaGetResolver() {
companion object { companion object {
val INSTANCE = LibraryMangaGetResolver() val INSTANCE = LibraryMangaGetResolver()

View File

@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor import android.database.Cursor
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.models.ChapterStorIOSQLiteGetResolver import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.MangaChapter import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() { class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
@ -12,15 +12,15 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
val INSTANCE = MangaChapterGetResolver() val INSTANCE = MangaChapterGetResolver()
} }
private val mangaGetResolver = MangaStorIOSQLiteGetResolver() private val mangaGetResolver = MangaGetResolver()
private val chapterGetResolver = ChapterStorIOSQLiteGetResolver() private val chapterGetResolver = ChapterGetResolver()
override fun mapFromCursor(cursor: Cursor): MangaChapter { override fun mapFromCursor(cursor: Cursor): MangaChapter {
val manga = mangaGetResolver.mapFromCursor(cursor) val manga = mangaGetResolver.mapFromCursor(cursor)
val chapter = chapterGetResolver.mapFromCursor(cursor) val chapter = chapterGetResolver.mapFromCursor(cursor)
manga.id = chapter.manga_id manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl")); manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
return MangaChapter(manga, chapter) return MangaChapter(manga, chapter)
} }

View File

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
companion object {
val INSTANCE = MangaChapterHistoryGetResolver()
}
/**
* Manga get resolver
*/
private val mangaGetResolver = MangaGetResolver()
/**
* Chapter get resolver
*/
private val chapterResolver = ChapterGetResolver()
/**
* History get resolver
*/
private val historyGetResolver = HistoryGetResolver()
/**
* Map correct objects from cursor result
*/
override fun mapFromCursor(cursor: Cursor): MangaChapterHistory {
// Get manga object
val manga = mangaGetResolver.mapFromCursor(cursor)
// Get chapter object
val chapter = chapterResolver.mapFromCursor(cursor)
// Get history object
val history = historyGetResolver.mapFromCursor(cursor)
// Make certain column conflicts are dealt with
manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
chapter.id = history.chapter_id
// Return result
return MangaChapterHistory(manga, chapter, history)
}
}

View File

@ -15,7 +15,7 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
val updateQuery = mapToUpdateQuery(manga) val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga) val contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues) val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
} }

View File

@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.data.database.tables
object HistoryTable {
/**
* Table name
*/
const val TABLE = "history"
/**
* Id column name
*/
const val COL_ID = "${TABLE}_id"
/**
* Chapter id column name
*/
const val COL_CHAPTER_ID = "${TABLE}_chapter_id"
/**
* Last read column name
*/
const val COL_LAST_READ = "${TABLE}_last_read"
/**
* Time read column name
*/
const val COL_TIME_READ = "${TABLE}_time_read"
/**
* query to create history table
*/
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
$COL_LAST_READ LONG,
$COL_TIME_READ LONG,
FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID})
ON DELETE CASCADE
)"""
/**
* query to index history chapter id
*/
val createChapterIdIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)"
}

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()
@ -185,7 +187,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
private fun downloadChapter(download: Download): Observable<Download> { private fun downloadChapter(download: Download): Observable<Download> {
DiskUtils.createDirectory(download.directory) DiskUtils.createDirectory(download.directory)
val pageListObservable = if (download.pages == null) val pageListObservable: Observable<List<Page>> = if (download.pages == null)
// Pull page list from network and add them to download object // Pull page list from network and add them to download object
download.source.fetchPageListFromNetwork(download.chapter) download.source.fetchPageListFromNetwork(download.chapter)
.doOnNext { pages -> .doOnNext { pages ->
@ -258,21 +260,20 @@ class DownloadManager(private val context: Context, private val sourceManager: S
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> { private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE page.status = Page.DOWNLOAD_IMAGE
return source.imageResponse(page) return source.imageResponse(page)
.flatMap { .map {
val file = File(directory, filename)
try { try {
val file = File(directory, filename)
file.parentFile.mkdirs() file.parentFile.mkdirs()
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage()) it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
} catch (e: Exception) { } catch (e: Exception) {
it.body().close() it.close()
file.delete()
throw e throw e
} }
Observable.just(page) page
}
.retryWhen {
it.zipWith(Observable.range(1, 3)) { errors, retries -> retries }
.flatMap { retries -> Observable.timer((retries * 2).toLong(), TimeUnit.SECONDS) }
} }
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
} }
// 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
@ -323,7 +324,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
var actualProgress = 0 var actualProgress = 0
var status = Download.DOWNLOADED var status = Download.DOWNLOADED
// If any page has an error, the download result will be error // If any page has an error, the download result will be error
for (page in download.pages) { for (page in download.pages!!) {
actualProgress += page.progress actualProgress += page.progress
if (page.status != Page.READY) { if (page.status != Page.READY) {
status = Download.ERROR status = Download.ERROR
@ -376,7 +377,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
// Shortcut for the method above // Shortcut for the method above
private fun savePageList(download: Download) { private fun savePageList(download: Download) {
savePageList(download.source, download.manga, download.chapter, download.pages) savePageList(download.source, download.manga, download.chapter, download.pages!!)
} }
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File { fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {

View File

@ -79,7 +79,7 @@ class DownloadNotifier(private val context: Context) {
return return
} }
} else { } else {
if (download != null && download.pages.size == download.downloadedImages) { if (download != null && download.pages!!.size == download.downloadedImages) {
onComplete(download) onComplete(download)
return return
} }
@ -107,8 +107,8 @@ class DownloadNotifier(private val context: Context) {
setContentTitle(it.chapter.name) setContentTitle(it.chapter.name)
setContentText(context.getString(R.string.chapter_downloading_progress) setContentText(context.getString(R.string.chapter_downloading_progress)
.format(it.downloadedImages, it.pages.size)) .format(it.downloadedImages, it.pages!!.size))
setProgress(it.pages.size, it.downloadedImages, false) setProgress(it.pages!!.size, it.downloadedImages, false)
} }
} }

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

@ -1,55 +0,0 @@
package eu.kanade.tachiyomi.data.download.model;
import java.io.File;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.data.source.online.OnlineSource;
import rx.subjects.PublishSubject;
public class Download {
public OnlineSource source;
public Manga manga;
public Chapter chapter;
public List<Page> pages;
public File directory;
public transient volatile int totalProgress;
public transient volatile int downloadedImages;
private transient volatile int status;
private transient PublishSubject<Download> statusSubject;
public static final int NOT_DOWNLOADED = 0;
public static final int QUEUE = 1;
public static final int DOWNLOADING = 2;
public static final int DOWNLOADED = 3;
public static final int ERROR = 4;
public Download(OnlineSource source, Manga manga, Chapter chapter) {
this.source = source;
this.manga = manga;
this.chapter = chapter;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
notifyStatus();
}
public void setStatusSubject(PublishSubject<Download> subject) {
this.statusSubject = subject;
}
private void notifyStatus() {
if (statusSubject != null)
statusSubject.onNext(this);
}
}

View File

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.data.download.model
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import rx.subjects.PublishSubject
import java.io.File
class Download(val source: OnlineSource, val manga: Manga, val chapter: Chapter) {
lateinit var directory: File
var pages: List<Page>? = null
@Volatile @Transient var totalProgress: Int = 0
@Volatile @Transient var downloadedImages: Int = 0
@Volatile @Transient var status: Int = 0
set(status) {
field = status
statusSubject?.onNext(this)
}
@Transient private var statusSubject: PublishSubject<Download>? = null
fun setStatusSubject(subject: PublishSubject<Download>?) {
statusSubject = subject
}
companion object {
const val NOT_DOWNLOADED = 0
const val QUEUE = 1
const val DOWNLOADING = 2
const val DOWNLOADED = 3
const val ERROR = 4
}
}

View File

@ -6,29 +6,41 @@ import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue : CopyOnWriteArrayList<Download>() { class DownloadQueue(private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
: List<Download> by queue {
private val statusSubject = PublishSubject.create<Download>() private val statusSubject = PublishSubject.create<Download>()
override fun add(download: Download): Boolean { private val removeSubject = PublishSubject.create<Download>()
fun add(download: Download): Boolean {
download.setStatusSubject(statusSubject) download.setStatusSubject(statusSubject)
download.status = Download.QUEUE download.status = Download.QUEUE
return super.add(download) return queue.add(download)
} }
fun del(download: Download) { fun del(download: Download) {
super.remove(download) val removed = queue.remove(download)
download.setStatusSubject(null) download.setStatusSubject(null)
if (removed) {
removeSubject.onNext(download)
}
} }
fun del(chapter: Chapter) { fun del(chapter: Chapter) {
find { it.chapter.id == chapter.id }?.let { del(it) } find { it.chapter.id == chapter.id }?.let { del(it) }
} }
fun getActiveDownloads() = fun clear() {
queue.forEach { del(it) }
}
fun getActiveDownloads(): Observable<Download> =
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING } Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
fun getStatusObservable() = statusSubject.onBackpressureBuffer() fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
fun getRemovedObservable(): Observable<Download> = removeSubject.onBackpressureBuffer()
fun getProgressObservable(): Observable<Download> { fun getProgressObservable(): Observable<Download> {
return statusSubject.onBackpressureBuffer() return statusSubject.onBackpressureBuffer()

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

@ -4,7 +4,7 @@ import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileNotFoundException
import java.io.InputStream import java.io.InputStream
/** /**
@ -26,17 +26,32 @@ class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
@Throws(Exception::class) @Throws(Exception::class)
override fun loadData(priority: Priority): InputStream? { override fun loadData(priority: Priority): InputStream? {
if (manga.favorite) { if (manga.favorite) {
if (!file.exists()) { synchronized(file) {
file.parentFile.mkdirs() if (!file.exists()) {
networkFetcher.loadData(priority)?.let { val tmpFile = File(file.path + ".tmp")
it.use { input -> try {
file.outputStream().use { output -> // Retrieve source stream.
input.copyTo(output) val input = networkFetcher.loadData(priority)
?: throw Exception("Couldn't open source stream")
// Retrieve destination stream, create parent folders if needed.
val output = try {
tmpFile.outputStream()
} catch (e: FileNotFoundException) {
tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
} }
// Copy the file and rename to the original.
input.use { output.use { input.copyTo(output) } }
tmpFile.renameTo(file)
} catch (e: Exception) {
tmpFile.delete()
throw e
} }
} }
} }
return FileInputStream(file) return file.inputStream()
} else { } else {
if (file.exists()) { if (file.exists()) {
file.delete() file.delete()

View File

@ -1,18 +1,18 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import android.content.Context import android.content.Context
import android.util.LruCache
import com.bumptech.glide.Glide 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 +30,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.
@ -47,17 +47,13 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url * LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
* and the file where it should be stored in case the manga is a favorite. * and the file where it should be stored in case the manga is a favorite.
*/ */
private val modelCache = ModelCache<String, Pair<GlideUrl, File>>(100) private val lruCache = LruCache<String, Pair<GlideUrl, File>>(100)
/** /**
* Map where request headers are stored for a source. * Map where request headers are stored for a source.
*/ */
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.
*/ */
@ -79,6 +75,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
override fun getResourceFetcher(manga: Manga, override fun getResourceFetcher(manga: Manga,
width: Int, width: Int,
height: Int): DataFetcher<InputStream>? { height: Int): DataFetcher<InputStream>? {
// Check thumbnail is not null or empty // Check thumbnail is not null or empty
val url = manga.thumbnail_url val url = manga.thumbnail_url
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
@ -87,9 +84,9 @@ 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) = lruCache.get(url) ?:
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url)).apply { Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url!!)).apply {
modelCache.put(url, width, height, this) lruCache.put(url, this)
} }
// Get the network fetcher for this request url. // Get the network fetcher for this request url.
@ -108,7 +105,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT
return cachedHeaders.getOrPut(manga.source) { return cachedHeaders.getOrPut(manga.source) {
LazyHeaders.Builder().apply { LazyHeaders.Builder().apply {
setHeader("User-Agent", null as? String) val nullStr: String? = null
setHeader("User-Agent", nullStr)
for ((key, value) in source.headers.toMultimap()) { for ((key, value) in source.headers.toMultimap()) {
addHeader(key, value[0]) addHeader(key, value[0])
} }

View File

@ -7,7 +7,10 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.SystemClock import android.os.SystemClock
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.util.alarmManager import eu.kanade.tachiyomi.util.alarmManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/** /**
* This class is used to update the library by firing an alarm after a specified time. * This class is used to update the library by firing an alarm after a specified time.
@ -25,10 +28,8 @@ class LibraryUpdateAlarm : BroadcastReceiver() {
* @param intervalInHours the time in hours when it will be executed. Defaults to the * @param intervalInHours the time in hours when it will be executed. Defaults to the
* value stored in preferences. * value stored in preferences.
*/ */
@JvmStatic
@JvmOverloads
fun startAlarm(context: Context, fun startAlarm(context: Context,
intervalInHours: Int = PreferencesHelper.getLibraryUpdateInterval(context)) { intervalInHours: Int = Injekt.get<PreferencesHelper>().libraryUpdateInterval().getOrDefault()) {
// Stop previous running alarms if needed, and do not restart it if the interval is 0. // Stop previous running alarms if needed, and do not restart it if the interval is 0.
stopAlarm(context) stopAlarm(context)
if (intervalInHours == 0) if (intervalInHours == 0)

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

@ -55,7 +55,7 @@ class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interc
val js = operation val js = operation
//language=RegExp //language=RegExp
.replace(Regex("""a\.value =(.+?) \+ .+?;"""), "$1") .replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
//language=RegExp //language=RegExp
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "") .replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
.replace("\n", "") .replace("\n", "")

View File

@ -5,24 +5,49 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import rx.subscriptions.Subscriptions import rx.Producer
import java.io.IOException import rx.Subscription
import java.util.concurrent.atomic.AtomicBoolean
fun Call.asObservable(): Observable<Response> { fun Call.asObservable(): Observable<Response> {
return Observable.create { subscriber -> return Observable.create { subscriber ->
subscriber.add(Subscriptions.create { cancel() }) // Since Call is a one-shot type, clone it for each new subscriber.
val call = if (!isExecuted) this else {
// TODO use clone method in OkHttp 3.5
val field = javaClass.getDeclaredField("client").apply { isAccessible = true }
val client = field.get(this) as OkHttpClient
client.newCall(request())
}
try { // Wrap the call in a helper which handles both unsubscription and backpressure.
val response = execute() val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
if (!subscriber.isUnsubscribed) { override fun request(n: Long) {
subscriber.onNext(response) if (n == 0L || !compareAndSet(false, true)) return
subscriber.onCompleted()
try {
val response = call.execute()
if (!subscriber.isUnsubscribed) {
subscriber.onNext(response)
subscriber.onCompleted()
}
} catch (error: Exception) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error)
}
}
} }
} catch (error: IOException) {
if (!subscriber.isUnsubscribed) { override fun unsubscribe() {
subscriber.onError(error) call.cancel()
}
override fun isUnsubscribed(): Boolean {
return call.isCanceled
} }
} }
subscriber.add(requestArbiter)
subscriber.setProducer(requestArbiter)
} }
} }

View File

@ -10,13 +10,15 @@ import eu.kanade.tachiyomi.R
*/ */
class PreferenceKeys(context: Context) { class PreferenceKeys(context: Context) {
val theme = context.getString(R.string.pref_theme_key)
val rotation = context.getString(R.string.pref_rotation_type_key) val rotation = context.getString(R.string.pref_rotation_type_key)
val enableTransitions = context.getString(R.string.pref_enable_transitions_key) val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
val showPageNumber = context.getString(R.string.pref_show_page_number_key) val showPageNumber = context.getString(R.string.pref_show_page_number_key)
val hideStatusBar = context.getString(R.string.pref_hide_status_bar_key) val fullscreen = context.getString(R.string.pref_fullscreen_key)
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key) val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
@ -54,8 +56,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)
@ -80,6 +80,10 @@ class PreferenceKeys(context: Context) {
val filterUnread = context.getString(R.string.pref_filter_unread_key) val filterUnread = context.getString(R.string.pref_filter_unread_key)
val automaticUpdateStatus = context.getString(R.string.pref_enable_automatic_updates_key)
val startScreen = context.getString(R.string.pref_start_screen_key)
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId" fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId" fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"

View File

@ -13,7 +13,7 @@ import java.io.IOException
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!! fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
class PreferencesHelper(private val context: Context) { class PreferencesHelper(context: Context) {
val keys = PreferenceKeys(context) val keys = PreferenceKeys(context)
@ -32,28 +32,11 @@ class PreferencesHelper(private val context: Context) {
} }
} }
companion object { fun startScreen() = prefs.getInt(keys.startScreen, 1)
fun getLibraryUpdateInterval(context: Context): Int { fun clear() = prefs.edit().clear().apply()
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_library_update_interval_key), 0)
}
fun getAutomaticUpdateStatus(context: Context): Boolean { fun theme() = prefs.getInt(keys.theme, 1)
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.pref_enable_automatic_updates), false)
}
@JvmStatic
fun getTheme(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_theme_key), 1)
}
}
fun clear() {
prefs.edit().clear().apply()
}
fun rotation() = rxPrefs.getInteger(keys.rotation, 1) fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
@ -61,13 +44,13 @@ class PreferencesHelper(private val context: Context) {
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true) fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
fun hideStatusBar() = rxPrefs.getBoolean(keys.hideStatusBar, true) fun fullscreen() = rxPrefs.getBoolean(keys.fullscreen, true)
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true) fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false) fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
fun customBrightnessValue() = rxPrefs.getFloat(keys.customBrightnessValue, 0f) fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0)
fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1) fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
@ -101,8 +84,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"))
@ -149,4 +130,6 @@ class PreferencesHelper(private val context: Context) {
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false) fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
fun automaticUpdateStatus() = prefs.getBoolean(keys.automaticUpdateStatus, false)
} }

View File

@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.data.source
class Language(val code: String, val lang: String) class Language(val code: String, val lang: String)
val DE = Language("DE", "German")
val EN = Language("EN", "English") val EN = Language("EN", "English")
val RU = Language("RU", "Russian") val RU = Language("RU", "Russian")
fun getLanguages() = listOf(EN, RU) fun getLanguages() = listOf(DE, EN, RU)

View File

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
import eu.kanade.tachiyomi.data.source.online.english.* import eu.kanade.tachiyomi.data.source.online.english.*
import eu.kanade.tachiyomi.data.source.online.german.WieManga
import eu.kanade.tachiyomi.data.source.online.russian.Mangachan import eu.kanade.tachiyomi.data.source.online.russian.Mangachan
import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga
import eu.kanade.tachiyomi.data.source.online.russian.Readmanga import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
@ -25,8 +26,10 @@ open class SourceManager(private val context: Context) {
val MINTMANGA = 6 val MINTMANGA = 6
val MANGACHAN = 7 val MANGACHAN = 7
val READMANGATODAY = 8 val READMANGATODAY = 8
val MANGASEE = 9
val WIEMANGA = 10
val LAST_SOURCE = 8 val LAST_SOURCE = 10
val sourcesMap = createSources() val sourcesMap = createSources()
@ -45,6 +48,8 @@ open class SourceManager(private val context: Context) {
MINTMANGA -> Mintmanga(context, id) MINTMANGA -> Mintmanga(context, id)
MANGACHAN -> Mangachan(context, id) MANGACHAN -> Mangachan(context, id)
READMANGATODAY -> Readmangatoday(context, id) READMANGATODAY -> Readmangatoday(context, id)
MANGASEE -> Mangasee(context, id)
WIEMANGA -> WieManga(context, id)
else -> null else -> null
} }

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.data.source.model;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class MangasPage {
public List<Manga> mangas;
public int page;
public String url;
public String nextPageUrl;
public MangasPage(int page) {
this.page = page;
}
}

View File

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.data.source.model
import eu.kanade.tachiyomi.data.database.models.Manga
class MangasPage(val page: Int) {
val mangas: MutableList<Manga> = mutableListOf()
lateinit var url: String
var nextPageUrl: String? = null
}

View File

@ -1,105 +0,0 @@
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 rx.subjects.PublishSubject;
public class Page implements ProgressListener {
private int pageNumber;
private String url;
private String imageUrl;
private transient Chapter chapter;
private transient String imagePath;
private transient volatile int status;
private transient volatile int progress;
private transient PublishSubject<Integer> statusSubject;
public static final int QUEUE = 0;
public static final int LOAD_PAGE = 1;
public static final int DOWNLOAD_IMAGE = 2;
public static final int READY = 3;
public static final int ERROR = 4;
public Page(int pageNumber, String url) {
this(pageNumber, url, null, null);
}
public Page(int pageNumber, String url, String imageUrl) {
this(pageNumber, url, imageUrl, null);
}
public Page(int pageNumber, String url, String imageUrl, String imagePath) {
this.pageNumber = pageNumber;
this.url = url;
this.imageUrl = imageUrl;
this.imagePath = imagePath;
}
public int getPageNumber() {
return pageNumber;
}
public String getUrl() {
return url;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getImagePath() {
return imagePath;
}
public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
if (statusSubject != null)
statusSubject.onNext(status);
}
public int getProgress() {
return progress;
}
public void setProgress(int value) {
progress = value;
}
@Override
public void update(long bytesRead, long contentLength, boolean done) {
progress = (int) ((100 * bytesRead) / contentLength);
}
public void setStatusSubject(PublishSubject<Integer> subject) {
this.statusSubject = subject;
}
public Chapter getChapter() {
return chapter;
}
public void setChapter(Chapter chapter) {
this.chapter = chapter;
}
public boolean isLastPage() {
List<Page> chapterPages = chapter.getPages();
return chapterPages.size() -1 == pageNumber;
}
}

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.data.source.model
import eu.kanade.tachiyomi.data.network.ProgressListener
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
import rx.subjects.Subject
class Page(
val pageNumber: Int,
val url: String,
var imageUrl: String? = null,
@Transient var imagePath: String? = null
) : ProgressListener {
@Transient lateinit var chapter: ReaderChapter
@Transient @Volatile var status: Int = 0
set(value) {
field = value
statusSubject?.onNext(value)
}
@Transient @Volatile var progress: Int = 0
@Transient private var statusSubject: Subject<Int, Int>? = null
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
progress = (100 * bytesRead / contentLength).toInt()
}
fun setStatusSubject(subject: Subject<Int, Int>?) {
this.statusSubject = subject
}
companion object {
const val QUEUE = 0
const val LOAD_PAGE = 1
const val DOWNLOAD_IMAGE = 2
const val READY = 3
const val ERROR = 4
}
}

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,13 @@ 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 okhttp3.* import eu.kanade.tachiyomi.util.UrlUtil
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
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 +31,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 +64,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.
*/ */
@ -89,10 +87,8 @@ abstract class OnlineSource(context: Context) : Source {
.newCall(popularMangaRequest(page)) .newCall(popularMangaRequest(page))
.asObservable() .asObservable()
.map { response -> .map { response ->
page.apply { popularMangaParse(response, page)
mangas = mutableListOf<Manga>() page
popularMangaParse(response, this)
}
} }
/** /**
@ -134,10 +130,8 @@ abstract class OnlineSource(context: Context) : Source {
.newCall(searchMangaRequest(page, query)) .newCall(searchMangaRequest(page, query))
.asObservable() .asObservable()
.map { response -> .map { response ->
page.apply { searchMangaParse(response, page, query)
mangas = mutableListOf<Manga>() page
searchMangaParse(response, this, query)
}
} }
/** /**
@ -363,7 +357,7 @@ abstract class OnlineSource(context: Context) : Source {
* @param page the chapter whose page list has to be fetched * @param page the chapter whose page list has to be fetched
*/ */
open protected fun imageRequest(page: Page): Request { open protected fun imageRequest(page: Page): Request {
return GET(page.imageUrl, headers) return GET(page.imageUrl!!, headers)
} }
/** /**
@ -373,20 +367,18 @@ abstract class OnlineSource(context: Context) : Source {
* @param page the page. * @param page the page.
*/ */
fun getCachedImage(page: Page): Observable<Page> { fun getCachedImage(page: Page): Observable<Page> {
val pageObservable = Observable.just(page) val imageUrl = page.imageUrl ?: return Observable.just(page)
if (page.imageUrl.isNullOrEmpty())
return pageObservable
return pageObservable return Observable.just(page)
.flatMap { .flatMap {
if (!chapterCache.isImageInCache(page.imageUrl)) { if (!chapterCache.isImageInCache(imageUrl)) {
cacheImage(page) cacheImage(page)
} else { } else {
Observable.just(page) Observable.just(page)
} }
} }
.doOnNext { .doOnNext {
page.imagePath = chapterCache.getImagePath(page.imageUrl) page.imagePath = chapterCache.getImagePath(imageUrl)
page.status = Page.READY page.status = Page.READY
} }
.doOnError { page.status = Page.ERROR } .doOnError { page.status = Page.ERROR }
@ -401,32 +393,13 @@ abstract class OnlineSource(context: Context) : Source {
private fun cacheImage(page: Page): Observable<Page> { private fun cacheImage(page: Page): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE page.status = Page.DOWNLOAD_IMAGE
return imageResponse(page) return imageResponse(page)
.doOnNext { chapterCache.putImageToCache(page.imageUrl, it, preferences.reencodeImage()) } .doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it, preferences.reencodeImage()) }
.map { page } .map { page }
} }
// Utility methods // Utility methods
/**
* Returns an absolute url from a href.
*
* Ex:
* href="http://example.com/foo" url="http://example.com" -> http://example.com/foo
* href="/mypath" url="http://example.com/foo" -> http://example.com/mypath
* href="bar" url="http://example.com/foo" -> http://example.com/bar
* href="bar" url="http://example.com/foo/" -> http://example.com/foo/bar
*
* @param href the href attribute from the html.
* @param url the requested url.
*/
fun getAbsoluteUrl(href: String, url: HttpUrl) = when {
href.startsWith("http://") || href.startsWith("https://") -> href
href.startsWith("/") -> url.newBuilder().encodedPath("/").fragment(null).query(null)
.toString() + href.substring(1)
else -> url.toString().substringBeforeLast('/') + "/$href"
}
fun fetchAllImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages) fun fetchAllImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() } .filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages)) .mergeWith(fetchRemainingImageUrlsFromPageList(pages))
@ -441,6 +414,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

@ -5,8 +5,8 @@ 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.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.asJsoup
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -24,19 +24,16 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param page the page object to be filled. * @param page the page object to be filled.
*/ */
override fun popularMangaParse(response: Response, page: MangasPage) { override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string()) val document = response.asJsoup()
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)
} }
} }
popularMangaNextPageSelector()?.let { selector -> popularMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let { page.nextPageUrl = document.select(selector).first()?.absUrl("href")
getAbsoluteUrl(it, response.request().url())
}
} }
} }
@ -68,19 +65,16 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param query the search query. * @param query the search query.
*/ */
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 = response.asJsoup()
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)
} }
} }
searchMangaNextPageSelector()?.let { selector -> searchMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let { page.nextPageUrl = document.select(selector).first()?.absUrl("href")
getAbsoluteUrl(it, response.request().url())
}
} }
} }
@ -111,7 +105,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param manga the manga to fill. * @param manga the manga to fill.
*/ */
override fun mangaDetailsParse(response: Response, manga: Manga) { override fun mangaDetailsParse(response: Response, manga: Manga) {
mangaDetailsParse(Jsoup.parse(response.body().string()), manga) mangaDetailsParse(response.asJsoup(), manga)
} }
/** /**
@ -129,7 +123,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param chapters the list of chapters to fill. * @param chapters the list of chapters to fill.
*/ */
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) { override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
val document = Jsoup.parse(response.body().string()) val document = response.asJsoup()
for (element in document.select(chapterListSelector())) { for (element in document.select(chapterListSelector())) {
Chapter.create().apply { Chapter.create().apply {
@ -159,7 +153,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param pages the list of pages to fill. * @param pages the list of pages to fill.
*/ */
override fun pageListParse(response: Response, pages: MutableList<Page>) { override fun pageListParse(response: Response, pages: MutableList<Page>) {
pageListParse(Jsoup.parse(response.body().string()), pages) pageListParse(response.asJsoup(), pages)
} }
/** /**
@ -176,7 +170,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String {
return imageUrlParse(Jsoup.parse(response.body().string())) return imageUrlParse(response.asJsoup())
} }
/** /**

View File

@ -8,6 +8,8 @@ import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.getLanguages import eu.kanade.tachiyomi.data.source.getLanguages
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.asJsoup
import eu.kanade.tachiyomi.util.attrOrText
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
@ -52,20 +54,17 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
override fun popularMangaInitialUrl() = map.popular.url override fun popularMangaInitialUrl() = map.popular.url
override fun popularMangaParse(response: Response, page: MangasPage) { override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string()) val document = response.asJsoup()
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)
} }
} }
map.popular.next_url_css?.let { selector -> map.popular.next_url_css?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let { page.nextPageUrl = document.select(selector).first()?.absUrl("href")
getAbsoluteUrl(it, response.request().url())
}
} }
} }
@ -82,25 +81,22 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query) override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query)
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 = response.asJsoup()
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)
} }
} }
map.search.next_url_css?.let { selector -> map.search.next_url_css?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let { page.nextPageUrl = document.select(selector).first()?.absUrl("href")
getAbsoluteUrl(it, response.request().url())
}
} }
} }
override fun mangaDetailsParse(response: Response, manga: Manga) { override fun mangaDetailsParse(response: Response, manga: Manga) {
val document = Jsoup.parse(response.body().string()) val document = response.asJsoup()
with(map.manga) { with(map.manga) {
val pool = parts.get(document) val pool = parts.get(document)
@ -114,7 +110,7 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
} }
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) { override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
val document = Jsoup.parse(response.body().string()) val document = response.asJsoup()
with(map.chapters) { with(map.chapters) {
val pool = emptyMap<String, Element>() val pool = emptyMap<String, Element>()
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH) val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
@ -123,7 +119,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
@ -133,32 +129,59 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
} }
override fun pageListParse(response: Response, pages: MutableList<Page>) { override fun pageListParse(response: Response, pages: MutableList<Page>) {
val document = Jsoup.parse(response.body().string()) val body = response.body().string()
val url = response.request().url().toString()
// TODO lazy initialization in Kotlin 1.1
val document = Jsoup.parse(body, url)
with(map.pages) { with(map.pages) {
val url = response.request().url().toString() // Capture a list of values where page urls will be resolved.
pages_css?.let { val capturedPages = if (pages_regex != null)
for (element in document.select(it)) { pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
val value = element.attr(pages_attr) else if (pages_css != null)
val pageUrl = replace?.let { url.replace(it.toRegex(), replacement!!.replace("\$value", value)) } ?: value document.select(pages_css).map { it.attrOrText(pages_attr!!) }
pages.add(Page(pages.size, pageUrl)) else
} null
// For each captured value, obtain the url and create a new page.
capturedPages?.forEach { value ->
// If the captured value isn't an url, we have to use replaces with the chapter url.
val pageUrl = if (replace != null && replacement != null)
url.replace(replace!!.toRegex(), replacement!!.replace("\$value", value))
else
value
pages.add(Page(pages.size, pageUrl))
} }
for ((i, element) in document.select(image_css).withIndex()) { // Capture a list of images.
pages.getOrNull(i)?.imageUrl = element.attr(image_attr).let { val capturedImages = if (image_regex != null)
getAbsoluteUrl(it, response.request().url()) image_regex!!.toRegex().findAll(body).map { it.groups[1]?.value }.toList()
} else if (image_css != null)
document.select(image_css).map { it.absUrl(image_attr) }
else
null
// Assign the image url to each page
capturedImages?.forEachIndexed { i, url ->
val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
page.imageUrl = url
} }
} }
} }
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String {
val document = Jsoup.parse(response.body().string()) val body = response.body().string()
return with(map.pages) { val url = response.request().url().toString()
document.select(image_css).first().attr(image_attr).let {
getAbsoluteUrl(it, response.request().url()) with(map.pages) {
} return if (image_regex != null)
image_regex!!.toRegex().find(body)!!.groups[1]!!.value
else if (image_css != null)
Jsoup.parse(body, url).select(image_css).first().absUrl(image_attr)
else
throw Exception("image_regex and image_css are null")
} }
} }

View File

@ -194,6 +194,9 @@ class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
class PagesNode(private val map: Map<String, Any?>) { class PagesNode(private val map: Map<String, Any?>) {
val pages_regex: String?
get() = map["pages_regex"] as? String
val pages_css: String? val pages_css: String?
get() = map["pages_css"] as? String get() = map["pages_css"] as? String
@ -206,7 +209,11 @@ class PagesNode(private val map: Map<String, Any?>) {
val replacement: String? val replacement: String?
get() = map["url_replacement"] as? String get() = map["url_replacement"] as? String
val image_css: String by map val image_regex: String?
get() = map["image_regex"] as? String
val image_css: String?
get() = map["image_css"] as? String
val image_attr: String val image_attr: String
get() = map["image_attr"] as? String ?: "src" get() = map["image_attr"] as? String ?: "src"

View File

@ -14,11 +14,11 @@ 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.data.source.online.LoginSource import eu.kanade.tachiyomi.data.source.online.LoginSource
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.selectText import eu.kanade.tachiyomi.util.selectText
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
@ -60,10 +60,9 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1" override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1"
override fun popularMangaParse(response: Response, page: MangasPage) { override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string()) val document = response.asJsoup()
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)
} }
@ -74,11 +73,11 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
} }
} }
override fun popularMangaSelector() = "tr:not([id]):not([class])" override fun popularMangaSelector() = "tr:has(a)"
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()
} }
} }
@ -88,10 +87,9 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=1" override fun searchMangaInitialUrl(query: String) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=1"
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 = response.asJsoup()
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)
} }
@ -141,7 +139,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
throw Exception(notice) throw Exception(notice)
} }
val document = Jsoup.parse(body) val document = response.asJsoup(body)
for (element in document.select(chapterListSelector())) { for (element in document.select(chapterListSelector())) {
Chapter.create().apply { Chapter.create().apply {
@ -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)
@ -223,11 +221,11 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun login(username: String, password: String) = override fun login(username: String, password: String) =
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global&section=login", headers)) client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global&section=login", headers))
.asObservable() .asObservable()
.flatMap { doLogin(it.body().string(), username, password) } .flatMap { doLogin(it, username, password) }
.map { isAuthenticationSuccessful(it) } .map { isAuthenticationSuccessful(it) }
private fun doLogin(response: String, username: String, password: String): Observable<Response> { private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
val doc = Jsoup.parse(response) val doc = response.asJsoup()
val form = doc.select("#login").first() val form = doc.select("#login").first()
val url = form.attr("action") val url = form.attr("action")
val authKey = form.select("input[name=auth_key]").first() val authKey = form.select("input[name=auth_key]").first()

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

@ -7,8 +7,8 @@ import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.ParseException import java.text.ParseException
@ -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
} }
@ -105,7 +105,7 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
} }
override fun pageListParse(response: Response, pages: MutableList<Page>) { override fun pageListParse(response: Response, pages: MutableList<Page>) {
val document = Jsoup.parse(response.body().string()) val document = response.asJsoup()
val url = response.request().url().toString().substringBeforeLast('/') val url = response.request().url().toString().substringBeforeLast('/')
document.select("select.m").first().select("option:not([value=0])").forEach { document.select("select.m").first().select("option:not([value=0])").forEach {

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
} }
@ -102,7 +102,7 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
} }
override fun pageListParse(document: Document, pages: MutableList<Page>) { override fun pageListParse(document: Document, pages: MutableList<Page>) {
document.select("select.wid60").first().getElementsByTag("option").forEach { document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
pages.add(Page(pages.size, it.attr("value"))) pages.add(Page(pages.size, it.attr("value")))
} }
pages.getOrNull(0)?.imageUrl = imageUrlParse(document) pages.getOrNull(0)?.imageUrl = imageUrlParse(document)

View File

@ -0,0 +1,125 @@
package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.*
import java.util.regex.Pattern
class Mangasee(context: Context, override val id: Int) : ParsedOnlineSource(context) {
override val name = "Mangasee"
override val baseUrl = "http://www.mangasee.co"
override val lang: Language get() = EN
private val datePattern = Pattern.compile("(\\d+)\\s+(.*?)s? ago.*")
private val dateFields = HashMap<String, Int>().apply {
put("second", Calendar.SECOND)
put("minute", Calendar.MINUTE)
put("hour", Calendar.HOUR)
put("day", Calendar.DATE)
put("week", Calendar.WEEK_OF_YEAR)
put("month", Calendar.MONTH)
put("year", Calendar.YEAR)
}
override fun popularMangaInitialUrl() = "$baseUrl/search_result.php?Action=Yes&order=popularity&numResultPerPage=20&sort=desc"
override fun popularMangaSelector() = "div.well > table > tbody > tr"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("td > h2 > a").first().let {
manga.setUrlWithoutDomain("/${it.attr("href")}")
manga.title = it.text()
}
}
override fun popularMangaNextPageSelector() = "ul.pagination > li > a:contains(Next)"
override fun searchMangaInitialUrl(query: String) =
"$baseUrl/advanced-search/result.php?sortBy=alphabet&direction=ASC&textOnly=no&resPerPage=20&page=1&seriesName=$query"
override fun searchMangaSelector() = "div.row > div > div > div > h1"
override fun searchMangaFromElement(element: Element, manga: Manga) {
element.select("a").first().let {
manga.setUrlWithoutDomain("/${it.attr("href")}")
manga.title = it.text()
}
}
override fun searchMangaNextPageSelector() = "ul.pagination > li > a:contains(Next)"
override fun mangaDetailsParse(document: Document, manga: Manga) {
val detailElement = document.select("div.well > div.row").first()
manga.author = detailElement.select("a[href^=../search_result.php?author_name=]").first()?.text()
manga.genre = detailElement.select("div > div.row > div:has(b:contains(Genre:)) > a").map { it.text() }.joinToString()
manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
manga.status = detailElement.select("div > div.row > div:has(b:contains(Scanlation Status:))").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
}
private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING
status.contains("Completed") -> Manga.COMPLETED
else -> Manga.UNKNOWN
}
override fun chapterListSelector() = "div.row > div > div.row:has(a.chapter_link[alt])"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first()
chapter.setUrlWithoutDomain("/${urlElement.attr("href")}")
chapter.name = urlElement.text()
chapter.date_upload = element.select("span").first()?.text()?.let { parseChapterDate(it) } ?: 0
}
private fun parseChapterDate(dateAsString: String): Long {
val m = datePattern.matcher(dateAsString)
if (m.matches()) {
val amount = Integer.parseInt(m.group(1))
val unit = m.group(2)
return Calendar.getInstance().apply {
add(dateFields[unit]!!, -amount)
}.time.time
} else {
return 0
}
}
override fun pageListParse(response: Response, pages: MutableList<Page>) {
val document = response.asJsoup()
val url = response.request().url().toString().substringBeforeLast('/')
val series = document.select("input[name=series]").first().attr("value")
val chapter = document.select("input[name=chapter]").first().attr("value")
val index = document.select("input[name=index]").first().attr("value")
document.select("select[name=page] > option").forEach {
pages.add(Page(pages.size, "$url/?series=$series&chapter=$chapter&index=$index&page=${pages.size + 1}"))
}
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
}
// Not used, overrides parent.
override fun pageListParse(document: Document, pages: MutableList<Page>) {
}
override fun imageUrlParse(document: Document) = document.select("div > a > img").attr("src")
}

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.source.Language
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.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -22,13 +23,15 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
override val lang: Language get() = EN override val lang: Language get() = EN
override val client: OkHttpClient get() = network.cloudflareClient
override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/" override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box" override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
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 +57,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 +86,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

@ -0,0 +1,97 @@
package eu.kanade.tachiyomi.data.source.online.german
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.DE
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
class WieManga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
override val name = "Wie Manga!"
override val baseUrl = "http://www.wiemanga.com"
override val lang: Language get() = DE
override fun popularMangaInitialUrl() = "$baseUrl/list/Hot-Book/"
override fun popularMangaSelector() = ".booklist td > div"
override fun popularMangaFromElement(element: Element, manga: Manga) {
val image = element.select("dt img")
val title = element.select("dd a:first-child")
manga.setUrlWithoutDomain(title.attr("href"))
manga.title = title.text()
manga.thumbnail_url = image.attr("src")
}
override fun popularMangaNextPageSelector() = null
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search/?wd=$query"
override fun searchMangaSelector() = ".searchresult td > div"
override fun searchMangaFromElement(element: Element, manga: Manga) {
val image = element.select(".resultimg img")
val title = element.select(".resultbookname")
manga.setUrlWithoutDomain(title.attr("href"))
manga.title = title.text()
manga.thumbnail_url = image.attr("src")
}
override fun searchMangaNextPageSelector() = ".pagetor a.l"
override fun mangaDetailsParse(document: Document, manga: Manga) {
val imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first()
val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").first()
manga.author = infoElement.select("dd:nth-of-type(2) a").first()?.text()
manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text()
manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "")
manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
if (manga.author == "RSS")
manga.author = null
if (manga.artist == "RSS")
manga.artist = null
}
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select(".col1 a").first()
val dateElement = element.select(".col3 a").first()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
}
private fun parseChapterDate(date: String): Long {
return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time
}
override fun pageListParse(response: Response, pages: MutableList<Page>) {
val document = response.asJsoup()
document.select("select#page").first().select("option").forEach {
pages.add(Page(pages.size, it.attr("value")))
}
}
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
}

View File

@ -17,7 +17,7 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con
override val name = "Mangachan" override val name = "Mangachan"
override val baseUrl = "http://mangachan.ru" override val baseUrl = "http://mangachan.me"
override val lang: Language get() = RU override val lang: Language get() = RU
@ -29,12 +29,9 @@ 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()
} }
element.select("img").first().let {
manga.thumbnail_url = baseUrl + it.attr("src")
}
} }
override fun popularMangaNextPageSelector() = "a:contains(Вперед)" override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
@ -50,11 +47,13 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun mangaDetailsParse(document: Document, manga: Manga) { override fun mangaDetailsParse(document: Document, manga: Manga) {
val infoElement = document.select("table.mangatitle").first() val infoElement = document.select("table.mangatitle").first()
val descElement = document.select("div#description").first() val descElement = document.select("div#description").first()
val imgElement = document.select("img#cover").first()
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text() manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text() manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text()) manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
manga.description = descElement.textNodes().first().text() manga.description = descElement.textNodes().first().text()
manga.thumbnail_url = baseUrl + imgElement.attr("src")
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int {
@ -70,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
@ -84,14 +84,14 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
val html = response.body().string() val html = response.body().string()
val beginIndex = html.indexOf("rm_h.init( [") val beginIndex = html.indexOf("rm_h.init( [")
val endIndex = html.indexOf("], 0, false);", beginIndex) val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex).replace("[\"\']+".toRegex(), "") val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+") val p = Pattern.compile("'.+?','.+?',\".+?\"")
val m = p.matcher(trimmedHtml) val m = p.matcher(trimmedHtml)
var i = 0 var i = 0
while (m.find()) { while (m.find()) {
val urlParts = m.group().split(',') val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2])) pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
} }
} }

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
@ -84,14 +84,14 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
val html = response.body().string() val html = response.body().string()
val beginIndex = html.indexOf("rm_h.init( [") val beginIndex = html.indexOf("rm_h.init( [")
val endIndex = html.indexOf("], 0, false);", beginIndex) val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex).replace("[\"\']+".toRegex(), "") val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+") val p = Pattern.compile("'.+?','.+?',\".+?\"")
val m = p.matcher(trimmedHtml) val m = p.matcher(trimmedHtml)
var i = 0 var i = 0
while (m.find()) { while (m.find()) {
val urlParts = m.group().split(',') val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2])) pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
} }
} }

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

@ -15,6 +15,8 @@ import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class UpdateDownloaderAlarm : BroadcastReceiver() { class UpdateDownloaderAlarm : BroadcastReceiver() {
@ -26,9 +28,8 @@ class UpdateDownloaderAlarm : BroadcastReceiver() {
* @param context the application context. * @param context the application context.
* @param intervalInHours the time in hours when it will be executed. * @param intervalInHours the time in hours when it will be executed.
*/ */
@JvmStatic fun startAlarm(context: Context, intervalInHours: Int = 12,
@JvmOverloads isEnabled: Boolean = Injekt.get<PreferencesHelper>().automaticUpdateStatus()) {
fun startAlarm(context: Context, intervalInHours: Int = 12, isEnabled: Boolean = PreferencesHelper.getAutomaticUpdateStatus(context)) {
// Stop previous running alarms if needed, and do not restart it if the interval is 0. // Stop previous running alarms if needed, and do not restart it if the interval is 0.
UpdateDownloaderAlarm.stopAlarm(context) UpdateDownloaderAlarm.stopAlarm(context)
if (intervalInHours == 0 || !isEnabled) if (intervalInHours == 0 || !isEnabled)

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 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 final ConcurrentHashMap<Class<?>, HashMap<Class<?>, Method>> cache = new ConcurrentHashMap<>();
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,65 +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.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(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

@ -8,8 +8,10 @@ import android.support.v4.content.ContextCompat
import android.support.v7.app.ActionBar import android.support.v7.app.ActionBar
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar import android.support.v7.widget.Toolbar
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
interface ActivityMixin { interface ActivityMixin {
@ -22,7 +24,7 @@ interface ActivityMixin {
} }
fun setAppTheme() { fun setAppTheme() {
setTheme(when (App.get(getActivity()).appTheme) { setTheme(when (Injekt.get<PreferencesHelper>().theme()) {
2 -> R.style.Theme_Tachiyomi_Dark 2 -> R.style.Theme_Tachiyomi_Dark
else -> R.style.Theme_Tachiyomi else -> R.style.Theme_Tachiyomi
}) })

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

@ -36,17 +36,20 @@ class CatalogueAdapter(val fragment: CatalogueFragment) : FlexibleAdapter<Catalo
* @param list the list to add. * @param list the list to add.
*/ */
fun addItems(list: List<Manga>) { fun addItems(list: List<Manga>) {
val sizeBeforeAdding = mItems.size if (list.isNotEmpty()) {
mItems.addAll(list) val sizeBeforeAdding = mItems.size
notifyItemRangeInserted(sizeBeforeAdding, list.size) mItems.addAll(list)
notifyItemRangeInserted(sizeBeforeAdding, list.size)
}
} }
/** /**
* Clears the list of manga from the adapter. * Clears the list of manga from the adapter.
*/ */
fun clear() { fun clear() {
val sizeBeforeRemoving = mItems.size
mItems.clear() mItems.clear()
notifyDataSetChanged() notifyItemRangeRemoved(0, sizeBeforeRemoving)
} }
/** /**
@ -56,7 +59,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

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.catalogue
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.support.v7.widget.GridLayoutManager import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
import android.support.v7.widget.Toolbar import android.support.v7.widget.Toolbar
import android.view.* import android.view.*
@ -24,7 +25,6 @@ import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.DividerItemDecoration import eu.kanade.tachiyomi.widget.DividerItemDecoration
import eu.kanade.tachiyomi.widget.EndlessScrollListener import eu.kanade.tachiyomi.widget.EndlessScrollListener
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
import kotlinx.android.synthetic.main.fragment_catalogue.* import kotlinx.android.synthetic.main.fragment_catalogue.*
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
@ -138,7 +138,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
catalogue_grid.adapter = adapter catalogue_grid.adapter = adapter
catalogue_grid.addOnScrollListener(gridScrollListener) catalogue_grid.addOnScrollListener(gridScrollListener)
val llm = NpaLinearLayoutManager(activity) val llm = LinearLayoutManager(activity)
listScrollListener = EndlessScrollListener(llm, { requestNextPage() }) listScrollListener = EndlessScrollListener(llm, { requestNextPage() })
catalogue_list.setHasFixedSize(true) catalogue_list.setHasFixedSize(true)
catalogue_list.adapter = adapter catalogue_list.adapter = adapter
@ -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.
@ -220,7 +220,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
private fun getMangasPageObservable(page: Int): Observable<List<Manga>> { private fun getMangasPageObservable(page: Int): Observable<List<Manga>> {
val nextMangasPage = MangasPage(page) val nextMangasPage = MangasPage(page)
if (page != 1) { if (page != 1) {
nextMangasPage.url = lastMangasPage!!.nextPageUrl nextMangasPage.url = lastMangasPage!!.nextPageUrl!!
} }
val observable = if (query.isEmpty()) val observable = if (query.isEmpty())

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.support.v7.widget.helper.ItemTouchHelper import android.support.v7.widget.helper.ItemTouchHelper
import android.view.Menu import android.view.Menu
@ -15,7 +16,6 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
import kotlinx.android.synthetic.main.activity_edit_categories.* import kotlinx.android.synthetic.main.activity_edit_categories.*
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
@ -50,7 +50,6 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
* *
* @param context context information. * @param context context information.
*/ */
@JvmStatic
fun newIntent(context: Context): Intent { fun newIntent(context: Context): Intent {
return Intent(context, CategoryActivity::class.java) return Intent(context, CategoryActivity::class.java)
} }
@ -70,7 +69,7 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
adapter = CategoryAdapter(this) adapter = CategoryAdapter(this)
// Create view and inject category items into view // Create view and inject category items into view
recycler.layoutManager = NpaLinearLayoutManager(this) recycler.layoutManager = LinearLayoutManager(this)
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter

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

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.download package eu.kanade.tachiyomi.ui.download
import android.os.Bundle import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.* import android.view.*
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
@ -8,13 +9,11 @@ import eu.kanade.tachiyomi.data.download.model.Download
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.util.plusAssign import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
import kotlinx.android.synthetic.main.fragment_download_queue.* import kotlinx.android.synthetic.main.fragment_download_queue.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
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.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -67,7 +66,6 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
* *
* @return a new instance of [DownloadFragment]. * @return a new instance of [DownloadFragment].
*/ */
@JvmStatic
fun newInstance(): DownloadFragment { fun newInstance(): DownloadFragment {
return DownloadFragment() return DownloadFragment()
} }
@ -93,7 +91,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
recycler.adapter = adapter recycler.adapter = adapter
// Set the layout manager for the recycler and fixed size. // Set the layout manager for the recycler and fixed size.
recycler.layoutManager = NpaLinearLayoutManager(activity) recycler.layoutManager = LinearLayoutManager(activity)
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
// Suscribe to changes // Suscribe to changes
@ -180,7 +178,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
* @param download the download to observe its progress. * @param download the download to observe its progress.
*/ */
private fun observeProgress(download: Download) { private fun observeProgress(download: Download) {
val subscription = Observable.interval(50, TimeUnit.MILLISECONDS, Schedulers.newThread()) val subscription = Observable.interval(50, TimeUnit.MILLISECONDS)
// Get the sum of percentages for all the pages. // Get the sum of percentages for all the pages.
.flatMap { .flatMap {
Observable.from(download.pages) Observable.from(download.pages)
@ -237,6 +235,10 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
adapter.setItems(downloads) adapter.setItems(downloads)
} }
fun onDownloadRemoved(position: Int) {
adapter.notifyItemRemoved(position)
}
/** /**
* Called when the progress of a download changes. * Called when the progress of a download changes.
* *
@ -262,7 +264,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

@ -26,18 +26,19 @@ class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
this.download = download this.download = download
// Update the chapter name. // Update the chapter name.
view.download_title.text = download.chapter.name view.chapter_title.text = download.chapter.name
// Update the manga title // Update the manga title
view.manga_title.text = download.manga.title view.manga_title.text = download.manga.title
// Update the progress bar and the number of downloaded pages // Update the progress bar and the number of downloaded pages
if (download.pages == null) { val pages = download.pages
if (pages == null) {
view.download_progress.progress = 0 view.download_progress.progress = 0
view.download_progress.max = 1 view.download_progress.max = 1
view.download_progress_text.text = "" view.download_progress_text.text = ""
} else { } else {
view.download_progress.max = download.pages.size * 100 view.download_progress.max = pages.size * 100
notifyProgress() notifyProgress()
notifyDownloadedPages() notifyDownloadedPages()
} }
@ -48,7 +49,7 @@ class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
*/ */
fun notifyProgress() { fun notifyProgress() {
if (view.download_progress.max == 1) { if (view.download_progress.max == 1) {
view.download_progress.max = download.pages.size * 100 view.download_progress.max = download.pages!!.size * 100
} }
view.download_progress.progress = download.totalProgress view.download_progress.progress = download.totalProgress
} }
@ -57,7 +58,7 @@ class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
* Updates the text field of the number of downloaded pages. * Updates the text field of the number of downloaded pages.
*/ */
fun notifyDownloadedPages() { fun notifyDownloadedPages() {
view.download_progress_text.text = "${download.downloadedImages}/${download.pages.size}" view.download_progress_text.text = "${download.downloadedImages}/${download.pages!!.size}"
} }
} }

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