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 |
|-------|----------|-------------|
| [![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)

View File

@ -38,9 +38,8 @@ android {
minSdkVersion 16
targetSdkVersion 23
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 8
versionCode project.findProperty('versionCode')?.toInteger() ?: 8
versionName "0.2.2"
versionCode 10
versionName "0.2.3"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -52,7 +51,7 @@ android {
buildTypes {
debug {
versionNameSuffix ".${getCommitCount()}"
versionNameSuffix "-${getCommitCount()}"
applicationIdSuffix ".debug"
}
release {
@ -79,59 +78,47 @@ android {
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 {
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
compile 'com.github.inorichi:subsampling-scale-image-view:421fb81'
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
// Android support library
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:customtabs:$SUPPORT_LIBRARY_VERSION"
final support_library_version = '23.4.0'
compile "com.android.support:support-v4:$support_library_version"
compile "com.android.support:appcompat-v7:$support_library_version"
compile "com.android.support:cardview-v7:$support_library_version"
compile "com.android.support:design:$support_library_version"
compile "com.android.support:recyclerview-v7:$support_library_version"
compile "com.android.support:support-annotations:$support_library_version"
compile "com.android.support:customtabs:$support_library_version"
// ReactiveX
compile 'io.reactivex:rxandroid:1.2.0'
compile 'io.reactivex:rxjava:1.1.5'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.1.6'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
// Network client
compile "com.squareup.okhttp3:okhttp:3.3.1"
// REST
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"
final retrofit_version = '2.1.0'
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// IO
compile 'com.squareup.okio:okio:1.8.0'
// 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
compile 'org.yaml:snakeyaml:1.17'
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
// JavaScript engine
compile 'com.squareup.duktape:duktape-android:0.9.5'
@ -146,19 +133,16 @@ dependencies {
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
compile "com.pushtorefresh.storio:sqlite:1.9.0"
// Model View Presenter
compile "info.android15.nucleus:nucleus:$NUCLEUS_VERSION"
compile "info.android15.nucleus:nucleus-support-v4:$NUCLEUS_VERSION"
compile "info.android15.nucleus:nucleus-support-v7:$NUCLEUS_VERSION"
final nucleus_version = '3.0.0'
compile "info.android15.nucleus:nucleus:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v4:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
// Dependency injection
compile "com.google.dagger:dagger:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
provided 'org.glassfish:javax.annotation:10.0-b28'
compile "uy.kohesive.injekt:injekt-core:1.16.1"
// Image library
compile 'com.github.bumptech.glide:glide:3.7.0'
@ -168,30 +152,30 @@ dependencies {
compile 'com.jakewharton.timber:timber:4.1.2'
// Crash reports
compile 'ch.acra:acra:4.8.5'
compile 'ch.acra:acra:4.9.0'
// UI
compile 'com.dmitrymalkovich.android:material-design-dimens:1.2'
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
compile 'eu.davidea:flexible-adapter:4.2.0'
compile 'com.nononsenseapps:filepicker:2.5.2'
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
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1'
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"
testCompile('org.robolectric:robolectric:3.0') {
exclude group: 'commons-logging', module: 'commons-logging'
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile 'org.robolectric:robolectric:3.1'
kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
buildscript {
ext.kotlin_version = '1.0.2'
ext.kotlin_version = '1.0.3'
repositories {
mavenCentral()
}

View File

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

View File

@ -1,15 +1,12 @@
package eu.kanade.tachiyomi
import android.app.Application
import android.content.Context
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector
import eu.kanade.tachiyomi.injection.component.AppComponent
import eu.kanade.tachiyomi.injection.component.DaggerAppComponent
import eu.kanade.tachiyomi.injection.module.AppModule
import org.acra.ACRA
import org.acra.annotation.ReportsCrashes
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar
@ReportsCrashes(
formUri = "http://tachiyomi.kanade.eu/crash_report",
@ -20,43 +17,18 @@ import timber.log.Timber
)
open class App : Application() {
lateinit var component: AppComponent
private set
lateinit var componentReflection: ComponentReflectionInjector<AppComponent>
private set
var appTheme = 0
override fun onCreate() {
super.onCreate()
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this))
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
component = prepareAppComponent().build()
componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
setupTheme()
setupAcra()
}
private fun setupTheme() {
appTheme = PreferencesHelper.getTheme(this)
}
protected open fun prepareAppComponent(): DaggerAppComponent.Builder {
return DaggerAppComponent.builder()
.appModule(AppModule(this))
}
protected open fun setupAcra() {
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
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
import java.io.*
import java.lang.reflect.Type
import java.util.*
/**
@ -191,8 +190,7 @@ class BackupManager(private val db: DatabaseHelper) {
private fun restoreCategories(jsonCategories: JsonArray) {
// Get categories from file and from db
val dbCategories = db.getCategories().executeAsBlocking()
val backupCategories = getArrayOrEmpty<Category>(jsonCategories,
object : TypeToken<List<Category>>() {}.type)
val backupCategories = gson.fromJson<List<CategoryImpl>>(jsonCategories)
// Iterate over them
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.
*/
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) {
// Map every entry to objects
val element = backupManga.asJsonObject
val manga = gson.fromJson(element.get(MANGA), Manga::class.java)
val chapters = getArrayOrEmpty<Chapter>(element.get(CHAPTERS), chapterToken)
val sync = getArrayOrEmpty<MangaSync>(element.get(MANGA_SYNC), mangaSyncToken)
val categories = getArrayOrEmpty<String>(element.get(CATEGORIES), categoriesNamesToken)
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray())
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
// Restore everything related to this manga
restoreManga(manga)
@ -340,7 +334,7 @@ class BackupManager(private val db: DatabaseHelper) {
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
// Fix foreign keys with the current manga id
for (mangaSync in sync) {
mangaSync.manga_id = manga.id
mangaSync.manga_id = manga.id!!
}
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 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.queries.*
@ -9,15 +10,16 @@ import eu.kanade.tachiyomi.data.database.queries.*
* This class provides operations to manage the database through its interfaces.
*/
open class DatabaseHelper(context: Context)
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries {
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(DbOpenHelper(context))
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
.addTypeMapping(MangaSync::class.java, MangaSyncSQLiteTypeMapping())
.addTypeMapping(Category::class.java, CategorySQLiteTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
.addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping())
.build()
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
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
internal().beginTransaction()
lowLevel().beginTransaction()
try {
block()
internal().setTransactionSuccessful()
lowLevel().setTransactionSuccessful()
} finally {
internal().endTransaction()
lowLevel().endTransaction()
}
}
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
internal().beginTransaction()
lowLevel().beginTransaction()
try {
val result = block()
internal().setTransactionSuccessful()
lowLevel().setTransactionSuccessful()
return result
} finally {
internal().endTransaction()
lowLevel().endTransaction()
}
}

View File

@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
/**
* Version of the database.
*/
const val DATABASE_VERSION = 2
const val DATABASE_VERSION = 3
}
override fun onCreate(db: SQLiteDatabase) = with(db) {
@ -26,11 +26,13 @@ class DbOpenHelper(context: Context)
execSQL(MangaSyncTable.createTableQuery)
execSQL(CategoryTable.createTableQuery)
execSQL(MangaCategoryTable.createTableQuery)
execSQL(HistoryTable.createTableQuery)
// DB indexes
execSQL(MangaTable.createUrlIndexQuery)
execSQL(MangaTable.createFavoriteIndexQuery)
execSQL(ChapterTable.createMangaIdIndexQuery)
execSQL(HistoryTable.createChapterIdIndexQuery)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@ -41,6 +43,11 @@ class DbOpenHelper(context: Context)
db.execSQL("""UPDATE mangas SET thumbnail_url =
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) {

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
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
@ -34,80 +33,6 @@ interface ChapterQueries : DbProvider {
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
// Add a delta to the chapter number, because binary decimal representation
// can retrieve the same chapter again
val chapterNumber = chapter.chapter_number + 0.00001
return db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.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 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.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.MangaTable as Manga
@ -39,6 +40,38 @@ fun getRecentsQuery() = """
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.

View File

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

View File

@ -15,7 +15,7 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
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
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.MangaStorIOSQLiteGetResolver
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
class LibraryMangaGetResolver : MangaGetResolver() {
companion object {
val INSTANCE = LibraryMangaGetResolver()

View File

@ -2,9 +2,9 @@ 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.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.MangaStorIOSQLiteGetResolver
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
@ -12,15 +12,15 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
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 {
val manga = mangaGetResolver.mapFromCursor(cursor)
val chapter = chapterGetResolver.mapFromCursor(cursor)
manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"));
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
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 contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
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.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.util.DiskUtils
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
import eu.kanade.tachiyomi.util.UrlUtil
import eu.kanade.tachiyomi.util.saveImageTo
import eu.kanade.tachiyomi.util.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -27,12 +24,17 @@ import rx.schedulers.Schedulers
import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileReader
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()
@ -185,7 +187,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
private fun downloadChapter(download: Download): Observable<Download> {
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
download.source.fetchPageListFromNetwork(download.chapter)
.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> {
page.status = Page.DOWNLOAD_IMAGE
return source.imageResponse(page)
.flatMap {
.map {
val file = File(directory, filename)
try {
val file = File(directory, filename)
file.parentFile.mkdirs()
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
} catch (e: Exception) {
it.body().close()
it.close()
file.delete()
throw e
}
Observable.just(page)
}
.retryWhen {
it.zipWith(Observable.range(1, 3)) { errors, retries -> retries }
.flatMap { retries -> Observable.timer((retries * 2).toLong(), TimeUnit.SECONDS) }
page
}
// 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
@ -323,7 +324,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
var actualProgress = 0
var status = Download.DOWNLOADED
// 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
if (page.status != Page.READY) {
status = Download.ERROR
@ -376,7 +377,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
// Shortcut for the method above
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 {

View File

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

View File

@ -7,14 +7,13 @@ import android.os.IBinder
import android.os.PowerManager
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.toast
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import javax.inject.Inject
import uy.kohesive.injekt.injectLazy
class DownloadService : Service() {
@ -29,8 +28,8 @@ class DownloadService : Service() {
}
}
@Inject lateinit var downloadManager: DownloadManager
@Inject lateinit var preferences: PreferencesHelper
val downloadManager: DownloadManager by injectLazy()
val preferences: PreferencesHelper by injectLazy()
private var wakeLock: PowerManager.WakeLock? = null
private var networkChangeSubscription: Subscription? = null
@ -39,7 +38,6 @@ class DownloadService : Service() {
override fun onCreate() {
super.onCreate()
App.get(this).component.inject(this)
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 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>()
override fun add(download: Download): Boolean {
private val removeSubject = PublishSubject.create<Download>()
fun add(download: Download): Boolean {
download.setStatusSubject(statusSubject)
download.status = Download.QUEUE
return super.add(download)
return queue.add(download)
}
fun del(download: Download) {
super.remove(download)
val removed = queue.remove(download)
download.setStatusSubject(null)
if (removed) {
removeSubject.onNext(download)
}
}
fun del(chapter: Chapter) {
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 }
fun getStatusObservable() = statusSubject.onBackpressureBuffer()
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
fun getRemovedObservable(): Observable<Download> = removeSubject.onBackpressureBuffer()
fun getProgressObservable(): Observable<Download> {
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.model.GlideUrl
import com.bumptech.glide.module.GlideModule
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
import javax.inject.Inject
/**
* Class used to update Glide module settings
*/
class AppGlideModule : GlideModule {
@Inject lateinit var networkHelper: NetworkHelper
override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
}
override fun registerComponents(context: Context, glide: Glide) {
App.get(context).component.inject(this)
glide.register(GlideUrl::class.java, InputStream::class.java,
OkHttpUrlLoader.Factory(networkHelper.client))
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory)
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 eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
/**
@ -26,17 +26,32 @@ class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
@Throws(Exception::class)
override fun loadData(priority: Priority): InputStream? {
if (manga.favorite) {
if (!file.exists()) {
file.parentFile.mkdirs()
networkFetcher.loadData(priority)?.let {
it.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
synchronized(file) {
if (!file.exists()) {
val tmpFile = File(file.path + ".tmp")
try {
// Retrieve source stream.
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 {
if (file.exists()) {
file.delete()

View File

@ -1,18 +1,18 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import android.util.LruCache
import com.bumptech.glide.Glide
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.*
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.database.models.Manga
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import uy.kohesive.injekt.injectLazy
import java.io.File
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.
@ -30,12 +30,12 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
/**
* Cover cache where persistent covers are stored.
*/
@Inject lateinit var coverCache: CoverCache
val coverCache: CoverCache by injectLazy()
/**
* Source manager.
*/
@Inject lateinit var sourceManager: SourceManager
val sourceManager: SourceManager by injectLazy()
/**
* 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
* 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.
*/
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
init {
App.get(context).component.inject(this)
}
/**
* Factory class for creating [MangaModelLoader] instances.
*/
@ -79,6 +75,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
override fun getResourceFetcher(manga: Manga,
width: Int,
height: Int): DataFetcher<InputStream>? {
// Check thumbnail is not null or empty
val url = manga.thumbnail_url
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
// and add them to the cache.
val (glideUrl, file) = modelCache.get(url, width, height) ?:
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url)).apply {
modelCache.put(url, width, height, this)
val (glideUrl, file) = lruCache.get(url) ?:
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url!!)).apply {
lruCache.put(url, this)
}
// 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
return cachedHeaders.getOrPut(manga.source) {
LazyHeaders.Builder().apply {
setHeader("User-Agent", null as? String)
val nullStr: String? = null
setHeader("User-Agent", nullStr)
for ((key, value) in source.headers.toMultimap()) {
addHeader(key, value[0])
}

View File

@ -7,7 +7,10 @@ import android.content.Context
import android.content.Intent
import android.os.SystemClock
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
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.
@ -25,10 +28,8 @@ class LibraryUpdateAlarm : BroadcastReceiver() {
* @param intervalInHours the time in hours when it will be executed. Defaults to the
* value stored in preferences.
*/
@JvmStatic
@JvmOverloads
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.
stopAlarm(context)
if (intervalInHours == 0)

View File

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

View File

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

View File

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

View File

@ -97,8 +97,8 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
MangaSync.create(this).apply {
title = it.selectText("title")
MangaSync.create(id).apply {
title = it.selectText("title")!!
remote_id = it.selectInt("id")
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()) }
.flatMap { Observable.from(it.select("manga")) }
.map {
MangaSync.create(this).apply {
title = it.selectText("series_title")
MangaSync.create(id).apply {
title = it.selectText("series_title")!!
remote_id = it.selectInt("series_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
status = it.selectInt("my_status")

View File

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

View File

@ -5,24 +5,49 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.subscriptions.Subscriptions
import java.io.IOException
import rx.Producer
import rx.Subscription
import java.util.concurrent.atomic.AtomicBoolean
fun Call.asObservable(): Observable<Response> {
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 {
val response = execute()
if (!subscriber.isUnsubscribed) {
subscriber.onNext(response)
subscriber.onCompleted()
// Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return
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) {
subscriber.onError(error)
override fun unsubscribe() {
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) {
val theme = context.getString(R.string.pref_theme_key)
val rotation = context.getString(R.string.pref_rotation_type_key)
val enableTransitions = context.getString(R.string.pref_enable_transitions_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)
@ -54,8 +56,6 @@ class PreferenceKeys(context: Context) {
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 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 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 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()!!
class PreferencesHelper(private val context: Context) {
class PreferencesHelper(context: 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 {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_library_update_interval_key), 0)
}
fun clear() = prefs.edit().clear().apply()
fun getAutomaticUpdateStatus(context: Context): Boolean {
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 theme() = prefs.getInt(keys.theme, 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 hideStatusBar() = rxPrefs.getBoolean(keys.hideStatusBar, true)
fun fullscreen() = rxPrefs.getBoolean(keys.fullscreen, true)
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
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)
@ -101,8 +84,6 @@ class PreferencesHelper(private val context: Context) {
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
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 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)
val DE = Language("DE", "German")
val EN = Language("EN", "English")
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.YamlOnlineSource
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.Mintmanga
import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
@ -25,8 +26,10 @@ open class SourceManager(private val context: Context) {
val MINTMANGA = 6
val MANGACHAN = 7
val READMANGATODAY = 8
val MANGASEE = 9
val WIEMANGA = 10
val LAST_SOURCE = 8
val LAST_SOURCE = 10
val sourcesMap = createSources()
@ -45,6 +48,8 @@ open class SourceManager(private val context: Context) {
MINTMANGA -> Mintmanga(context, id)
MANGACHAN -> Mangachan(context, id)
READMANGATODAY -> Readmangatoday(context, id)
MANGASEE -> Mangasee(context, id)
WIEMANGA -> WieManga(context, id)
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
import android.content.Context
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
@ -14,9 +13,13 @@ import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.model.MangasPage
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 javax.inject.Inject
import uy.kohesive.injekt.injectLazy
/**
* A simple implementation for sources from a website.
@ -28,17 +31,17 @@ abstract class OnlineSource(context: Context) : Source {
/**
* Network service.
*/
@Inject lateinit var network: NetworkHelper
val network: NetworkHelper by injectLazy()
/**
* Chapter cache.
*/
@Inject lateinit var chapterCache: ChapterCache
val chapterCache: ChapterCache by injectLazy()
/**
* 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
@ -61,11 +64,6 @@ abstract class OnlineSource(context: Context) : Source {
open val client: OkHttpClient
get() = network.client
init {
// Inject dependencies.
App.get(context).component.inject(this)
}
/**
* 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))
.asObservable()
.map { response ->
page.apply {
mangas = mutableListOf<Manga>()
popularMangaParse(response, this)
}
popularMangaParse(response, page)
page
}
/**
@ -134,10 +130,8 @@ abstract class OnlineSource(context: Context) : Source {
.newCall(searchMangaRequest(page, query))
.asObservable()
.map { response ->
page.apply {
mangas = mutableListOf<Manga>()
searchMangaParse(response, this, query)
}
searchMangaParse(response, page, query)
page
}
/**
@ -363,7 +357,7 @@ abstract class OnlineSource(context: Context) : Source {
* @param page the chapter whose page list has to be fetched
*/
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.
*/
fun getCachedImage(page: Page): Observable<Page> {
val pageObservable = Observable.just(page)
if (page.imageUrl.isNullOrEmpty())
return pageObservable
val imageUrl = page.imageUrl ?: return Observable.just(page)
return pageObservable
return Observable.just(page)
.flatMap {
if (!chapterCache.isImageInCache(page.imageUrl)) {
if (!chapterCache.isImageInCache(imageUrl)) {
cacheImage(page)
} else {
Observable.just(page)
}
}
.doOnNext {
page.imagePath = chapterCache.getImagePath(page.imageUrl)
page.imagePath = chapterCache.getImagePath(imageUrl)
page.status = Page.READY
}
.doOnError { page.status = Page.ERROR }
@ -401,32 +393,13 @@ abstract class OnlineSource(context: Context) : Source {
private fun cacheImage(page: Page): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE
return imageResponse(page)
.doOnNext { chapterCache.putImageToCache(page.imageUrl, it, preferences.reencodeImage()) }
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it, preferences.reencodeImage()) }
.map { page }
}
// 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)
.filter { !it.imageUrl.isNullOrEmpty() }
.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.
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.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
@ -24,19 +24,16 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param page the page object to be filled.
*/
override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string())
val document = response.asJsoup()
for (element in document.select(popularMangaSelector())) {
Manga().apply {
source = this@ParsedOnlineSource.id
Manga.create(id).apply {
popularMangaFromElement(element, this)
page.mangas.add(this)
}
}
popularMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
getAbsoluteUrl(it, response.request().url())
}
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
}
}
@ -68,19 +65,16 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param query the search query.
*/
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())) {
Manga().apply {
source = this@ParsedOnlineSource.id
Manga.create(id).apply {
searchMangaFromElement(element, this)
page.mangas.add(this)
}
}
searchMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
getAbsoluteUrl(it, response.request().url())
}
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
}
}
@ -111,7 +105,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param manga the manga to fill.
*/
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.
*/
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())) {
Chapter.create().apply {
@ -159,7 +153,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param pages the list of pages to fill.
*/
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.
*/
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.model.MangasPage
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.Response
import org.jsoup.Jsoup
@ -52,20 +54,17 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
override fun popularMangaInitialUrl() = map.popular.url
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)) {
Manga().apply {
source = this@YamlOnlineSource.id
Manga.create(id).apply {
title = element.text()
setUrl(element.attr("href"))
setUrlWithoutDomain(element.attr("href"))
page.mangas.add(this)
}
}
map.popular.next_url_css?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
getAbsoluteUrl(it, response.request().url())
}
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
}
}
@ -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 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)) {
Manga().apply {
source = this@YamlOnlineSource.id
Manga.create(id).apply {
title = element.text()
setUrl(element.attr("href"))
setUrlWithoutDomain(element.attr("href"))
page.mangas.add(this)
}
}
map.search.next_url_css?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
getAbsoluteUrl(it, response.request().url())
}
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
}
}
override fun mangaDetailsParse(response: Response, manga: Manga) {
val document = Jsoup.parse(response.body().string())
val document = response.asJsoup()
with(map.manga) {
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>) {
val document = Jsoup.parse(response.body().string())
val document = response.asJsoup()
with(map.chapters) {
val pool = emptyMap<String, Element>()
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
@ -123,7 +119,7 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
val chapter = Chapter.create()
element.select(title).first().let {
chapter.name = it.text()
chapter.setUrl(it.attr("href"))
chapter.setUrlWithoutDomain(it.attr("href"))
}
val dateElement = element.select(date?.select).first()
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>) {
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) {
val url = response.request().url().toString()
pages_css?.let {
for (element in document.select(it)) {
val value = element.attr(pages_attr)
val pageUrl = replace?.let { url.replace(it.toRegex(), replacement!!.replace("\$value", value)) } ?: value
pages.add(Page(pages.size, pageUrl))
}
// Capture a list of values where page urls will be resolved.
val capturedPages = if (pages_regex != null)
pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
else if (pages_css != null)
document.select(pages_css).map { it.attrOrText(pages_attr!!) }
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()) {
pages.getOrNull(i)?.imageUrl = element.attr(image_attr).let {
getAbsoluteUrl(it, response.request().url())
}
// Capture a list of images.
val capturedImages = if (image_regex != null)
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 {
val document = Jsoup.parse(response.body().string())
return with(map.pages) {
document.select(image_css).first().attr(image_attr).let {
getAbsoluteUrl(it, response.request().url())
}
val body = response.body().string()
val url = response.request().url().toString()
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?>) {
val pages_regex: String?
get() = map["pages_regex"] as? String
val pages_css: String?
get() = map["pages_css"] as? String
@ -206,7 +209,11 @@ class PagesNode(private val map: Map<String, Any?>) {
val replacement: 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
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.online.LoginSource
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.selectText
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
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 popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string())
val document = response.asJsoup()
for (element in document.select(popularMangaSelector())) {
Manga().apply {
source = this@Batoto.id
Manga.create(id).apply {
popularMangaFromElement(element, 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) {
element.select("a[href^=http://bato.to]").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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 searchMangaParse(response: Response, page: MangasPage, query: String) {
val document = Jsoup.parse(response.body().string())
val document = response.asJsoup()
for (element in document.select(searchMangaSelector())) {
Manga().apply {
source = this@Batoto.id
Manga.create(id).apply {
searchMangaFromElement(element, this)
page.mangas.add(this)
}
@ -141,7 +139,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
throw Exception(notice)
}
val document = Jsoup.parse(body)
val document = response.asJsoup(body)
for (element in document.select(chapterListSelector())) {
Chapter.create().apply {
@ -156,7 +154,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun chapterFromElement(element: Element, chapter: Chapter) {
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.date_upload = element.select("td").getOrNull(4)?.let {
parseDateFromElement(it)
@ -223,11 +221,11 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun login(username: String, password: String) =
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global&section=login", headers))
.asObservable()
.flatMap { doLogin(it.body().string(), username, password) }
.flatMap { doLogin(it, username, password) }
.map { isAuthenticationSuccessful(it) }
private fun doLogin(response: String, username: String, password: String): Observable<Response> {
val doc = Jsoup.parse(response)
private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
val doc = response.asJsoup()
val form = doc.select("#login").first()
val url = form.attr("action")
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) {
element.select("td a:eq(0)").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href"))
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
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.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
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) {
element.select("a.title").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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) {
element.select("a.series_preview").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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) {
val urlElement = element.select("a.tips").first()
chapter.setUrl(urlElement.attr("href"))
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
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>) {
val document = Jsoup.parse(response.body().string())
val document = response.asJsoup()
val url = response.request().url().toString().substringBeforeLast('/')
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) {
element.select("div.title > a").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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) {
element.select("a.manga_info").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href"))
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
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>) {
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.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.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.nodes.Document
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 client: OkHttpClient get() = network.cloudflareClient
override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("div.title > h2 > a").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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) {
element.select("div.title > h2 > a").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href"))
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.select("span.val").text()
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 baseUrl = "http://mangachan.ru"
override val baseUrl = "http://mangachan.me"
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) {
element.select("h2 > a").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
}
element.select("img").first().let {
manga.thumbnail_url = baseUrl + it.attr("src")
}
}
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) {
val infoElement = document.select("table.mangatitle").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.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
manga.description = descElement.textNodes().first().text()
manga.thumbnail_url = baseUrl + imgElement.attr("src")
}
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) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href"))
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = element.select("div.date").first()?.text()?.let {
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) {
element.select("h3 > a").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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) {
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.date_upload = element.select("td:eq(1)").first()?.text()?.let {
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 beginIndex = html.indexOf("rm_h.init( [")
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)
var i = 0
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]))
}
}

View File

@ -30,7 +30,7 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("h3 > a").first().let {
manga.setUrl(it.attr("href"))
manga.setUrlWithoutDomain(it.attr("href"))
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) {
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.date_upload = element.select("td:eq(1)").first()?.text()?.let {
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 beginIndex = html.indexOf("rm_h.init( [")
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)
var i = 0
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]))
}
}

View File

@ -8,7 +8,6 @@ import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
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.saveTo
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
import javax.inject.Inject
class UpdateDownloader(private val context: Context) :
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
@ -59,9 +58,6 @@ class UpdateDownloader(private val context: Context) :
private val notificationId: Int
get() = Constants.NOTIFICATION_UPDATER_ID
init {
App.get(context).component.inject(this)
}
/**
* Class containing download result

View File

@ -15,6 +15,8 @@ import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class UpdateDownloaderAlarm : BroadcastReceiver() {
@ -26,9 +28,8 @@ class UpdateDownloaderAlarm : BroadcastReceiver() {
* @param context the application context.
* @param intervalInHours the time in hours when it will be executed.
*/
@JvmStatic
@JvmOverloads
fun startAlarm(context: Context, intervalInHours: Int = 12, isEnabled: Boolean = PreferencesHelper.getAutomaticUpdateStatus(context)) {
fun startAlarm(context: Context, intervalInHours: Int = 12,
isEnabled: Boolean = Injekt.get<PreferencesHelper>().automaticUpdateStatus()) {
// Stop previous running alarms if needed, and do not restart it if the interval is 0.
UpdateDownloaderAlarm.stopAlarm(context)
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.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
import javax.inject.Inject
/**
* Presenter of [BackupFragment].
@ -20,7 +20,7 @@ class BackupPresenter : BasePresenter<BackupFragment>() {
/**
* Database.
*/
@Inject lateinit var db: DatabaseHelper
val db: DatabaseHelper by injectLazy()
/**
* 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.AppCompatActivity
import android.support.v7.widget.Toolbar
import eu.kanade.tachiyomi.App
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 {
@ -22,7 +24,7 @@ interface ActivityMixin {
}
fun setAppTheme() {
setTheme(when (App.get(getActivity()).appTheme) {
setTheme(when (Injekt.get<PreferencesHelper>().theme()) {
2 -> R.style.Theme_Tachiyomi_Dark
else -> R.style.Theme_Tachiyomi
})

View File

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

View File

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

View File

@ -36,17 +36,20 @@ class CatalogueAdapter(val fragment: CatalogueFragment) : FlexibleAdapter<Catalo
* @param list the list to add.
*/
fun addItems(list: List<Manga>) {
val sizeBeforeAdding = mItems.size
mItems.addAll(list)
notifyItemRangeInserted(sizeBeforeAdding, list.size)
if (list.isNotEmpty()) {
val sizeBeforeAdding = mItems.size
mItems.addAll(list)
notifyItemRangeInserted(sizeBeforeAdding, list.size)
}
}
/**
* Clears the list of manga from the adapter.
*/
fun clear() {
val sizeBeforeRemoving = mItems.size
mItems.clear()
notifyDataSetChanged()
notifyItemRangeRemoved(0, sizeBeforeRemoving)
}
/**
@ -56,7 +59,7 @@ class CatalogueAdapter(val fragment: CatalogueFragment) : FlexibleAdapter<Catalo
* @return an identifier for the item.
*/
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.os.Bundle
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.support.v7.widget.Toolbar
import android.view.*
@ -24,7 +25,6 @@ import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.DividerItemDecoration
import eu.kanade.tachiyomi.widget.EndlessScrollListener
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
import kotlinx.android.synthetic.main.fragment_catalogue.*
import kotlinx.android.synthetic.main.toolbar.*
import nucleus.factory.RequiresPresenter
@ -138,7 +138,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
catalogue_grid.adapter = adapter
catalogue_grid.addOnScrollListener(gridScrollListener)
val llm = NpaLinearLayoutManager(activity)
val llm = LinearLayoutManager(activity)
listScrollListener = EndlessScrollListener(llm, { requestNextPage() })
catalogue_list.setHasFixedSize(true)
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.
*/
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.subjects.PublishSubject
import timber.log.Timber
import javax.inject.Inject
import uy.kohesive.injekt.injectLazy
/**
* Presenter of [CatalogueFragment].
@ -29,22 +29,22 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
/**
* Source manager.
*/
@Inject lateinit var sourceManager: SourceManager
val sourceManager: SourceManager by injectLazy()
/**
* Database.
*/
@Inject lateinit var db: DatabaseHelper
val db: DatabaseHelper by injectLazy()
/**
* Preferences.
*/
@Inject lateinit var prefs: PreferencesHelper
val prefs: PreferencesHelper by injectLazy()
/**
* Cover cache.
*/
@Inject lateinit var coverCache: CoverCache
val coverCache: CoverCache by injectLazy()
/**
* Enabled sources.
@ -220,7 +220,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
private fun getMangasPageObservable(page: Int): Observable<List<Manga>> {
val nextMangasPage = MangasPage(page)
if (page != 1) {
nextMangasPage.url = lastMangasPage!!.nextPageUrl
nextMangasPage.url = lastMangasPage!!.nextPageUrl!!
}
val observable = if (query.isEmpty())

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.view.ActionMode
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.helper.ItemTouchHelper
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.adapter.FlexibleViewHolder
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.toolbar.*
import nucleus.factory.RequiresPresenter
@ -50,7 +50,6 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
*
* @param context context information.
*/
@JvmStatic
fun newIntent(context: Context): Intent {
return Intent(context, CategoryActivity::class.java)
}
@ -70,7 +69,7 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
adapter = CategoryAdapter(this)
// Create view and inject category items into view
recycler.layoutManager = NpaLinearLayoutManager(this)
recycler.layoutManager = LinearLayoutManager(this)
recycler.setHasFixedSize(true)
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.ui.base.presenter.BasePresenter
import rx.android.schedulers.AndroidSchedulers
import javax.inject.Inject
import uy.kohesive.injekt.injectLazy
/**
* Presenter of CategoryActivity.
@ -17,7 +17,7 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
/**
* Used to connect to database
*/
@Inject lateinit var db: DatabaseHelper
val db: DatabaseHelper by injectLazy()
/**
* List containing categories

View File

@ -35,7 +35,7 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHo
* @return an identifier for the item.
*/
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
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.*
import eu.kanade.tachiyomi.R
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.main.MainActivity
import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
import kotlinx.android.synthetic.main.fragment_download_queue.*
import nucleus.factory.RequiresPresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
import java.util.*
import java.util.concurrent.TimeUnit
@ -67,7 +66,6 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
*
* @return a new instance of [DownloadFragment].
*/
@JvmStatic
fun newInstance(): DownloadFragment {
return DownloadFragment()
}
@ -93,7 +91,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
recycler.adapter = adapter
// Set the layout manager for the recycler and fixed size.
recycler.layoutManager = NpaLinearLayoutManager(activity)
recycler.layoutManager = LinearLayoutManager(activity)
recycler.setHasFixedSize(true)
// Suscribe to changes
@ -180,7 +178,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
* @param download the download to observe its progress.
*/
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.
.flatMap {
Observable.from(download.pages)
@ -237,6 +235,10 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
adapter.setItems(downloads)
}
fun onDownloadRemoved(position: Int) {
adapter.notifyItemRemoved(position)
}
/**
* 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.
*/
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
// Update the chapter name.
view.download_title.text = download.chapter.name
view.chapter_title.text = download.chapter.name
// Update the manga title
view.manga_title.text = download.manga.title
// 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.max = 1
view.download_progress_text.text = ""
} else {
view.download_progress.max = download.pages.size * 100
view.download_progress.max = pages.size * 100
notifyProgress()
notifyDownloadedPages()
}
@ -48,7 +49,7 @@ class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
*/
fun notifyProgress() {
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
}
@ -57,7 +58,7 @@ class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
* Updates the text field of the number of downloaded pages.
*/
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