Compare commits

...

157 Commits

Author SHA1 Message Date
len
55de2b7d97 Remove gradle properties and fix travis build 2016-04-17 17:33:47 +02:00
len
065ada3d17 Update readme 2016-04-16 16:05:51 +02:00
len
0ee2bf5254 Release 0.2.0 2016-04-16 16:01:12 +02:00
len
0fe0088ff0 Also use no predictive animations for AutofitRecyclerView 2016-04-15 15:52:53 +02:00
len
492a24ec17 Use always 3 characters for downloaded pages. Fixes #181 2016-04-15 15:14:02 +02:00
len
17a6ea973e Some bugfixes 2016-04-15 14:58:36 +02:00
len
deaba48431 Fix a crash on older devices 2016-04-14 17:37:21 +02:00
len
eb662f1234 Fix some crashes when restoring backups 2016-04-14 17:24:34 +02:00
len
5ecdecea98 Skip memory cache for images in catalog 2016-04-13 17:59:05 +02:00
len
b4277faf90 Not yet #187 2016-04-13 17:42:53 +02:00
len
09902566ad Fix for #187? 2016-04-13 16:45:39 +02:00
len
dc80a5ffbd Minor XML refactoring 2016-04-13 16:42:59 +02:00
b1b97c19d4 Added option to check if connected to power before updating. closes #192 (#229) 2016-04-13 14:08:07 +02:00
len
46a0820e5c Fix scrolling issue 2016-04-13 02:08:43 +02:00
len
6cbdbb5be3 Fix #248 2016-04-12 22:41:45 +02:00
len
e753539c6d Bump subsampling version 2016-04-11 19:05:33 +02:00
len
b8d1a88623 Changes in cover cache. Store covers in external cache dir 2016-04-11 18:50:31 +02:00
len
b84635ffec Fix last commit 2016-04-10 20:14:35 +02:00
len
ed2fd00603 Style toolbar's spinner with light theme 2016-04-10 19:47:06 +02:00
len
af20c613a4 Hide upload date if not parsed 2016-04-10 17:53:02 +02:00
len
b27669ee32 Remove unused strings 2016-04-10 17:49:48 +02:00
len
81d39ea272 Remove unused attrs and colors. Theme preference dialogs on API >= 21 2016-04-10 17:34:10 +02:00
len
840437580f Fix for #144? 2016-04-10 16:19:05 +02:00
len
936ede9aba Light and dark theme are now using different accent colors 2016-04-10 15:48:57 +02:00
len
2fa5d0cbaf Add presenter subscriptions to the subscription list when using custom subscribe methods 2016-04-10 04:32:43 +02:00
len
e28f69cddf Dark theme now uses accent color for drawer items. #222 2016-04-10 01:37:42 +02:00
len
11f6c44442 Make status bar transparent on API > 21 properly. Snack function moved to an extension method in View 2016-04-10 00:59:12 +02:00
8c9db2db61 Merge pull request #245 from j2ghz/patch-1
Add caching to Travis
2016-04-09 17:57:55 +02:00
7e7d27505a Add caching to travis
https://docs.travis-ci.com/user/languages/android#Caching
2016-04-09 17:57:01 +02:00
len
b4211ddc0c Remove unneeded repository 2016-04-09 17:46:42 +02:00
len
ff906e8ee7 Move modified dependencies to another repository. Reorganize dependencies 2016-04-09 17:33:21 +02:00
c1ebccd0f4 Merge pull request #244 from j2ghz/master
Travis builds
2016-04-09 14:10:18 +02:00
9c223b48a3 rearrange badges on README 2016-04-09 13:37:11 +02:00
16037dd9bd try https://github.com/travis-ci/travis-ci/issues/4185 2016-04-09 11:52:36 +02:00
742e8c9f50 upgrade buildToolsVersion for SubsamplingScaleImageView 2016-04-09 11:45:07 +02:00
194cdf3b5f Travis 2016-04-09 11:38:59 +02:00
len
5fbeeade94 A few more crashes fixed 2016-04-08 22:57:31 +02:00
len
72f029b57f Fix #242. Minor changes 2016-04-08 18:01:33 +02:00
len
67c4781376 Upgrade support library. Switch gradle build tools and AS to 2.0. Adapt code to new support lib 2016-04-08 16:18:41 +02:00
len
fe49286d97 A few more crashes fixed 2016-04-07 20:39:37 +02:00
len
4196a0f585 Minor changes trying to fix a crash 2016-04-07 14:43:02 +02:00
len
a6a9b13545 Fix proguard rules 2016-04-06 21:31:35 +02:00
len
fa8d0946e9 Remove unneded dependency 2016-04-06 20:40:37 +02:00
len
1844b8c5a2 Add commit number to version name in debug version 2016-04-06 17:10:43 +02:00
len
7c503648ff Minor changes. Also fix #240 2016-04-06 16:45:13 +02:00
len
a598ebf72f More crash fixes 2016-04-06 02:18:04 +02:00
len
d8ac35d259 Crash fixes 2016-04-04 23:25:50 +02:00
len
5029e4a28c Fix a bug when opening a chapter from the recents tab and changing the viewer from the reader would not update chapters for that manga anymore 2016-04-03 18:47:56 +02:00
908e60dea4 Merge pull request #238 from j2ghz/patch-1
Fix formatting issues when people ignore instructions
2016-04-02 14:35:19 +02:00
d577bf300c Fix formatting issues when people ignore instructions 2016-04-02 14:34:27 +02:00
len
579a606f93 Upgrade dependencies. Downgrade material dialogs to avoid crash on older android versions 2016-04-02 14:30:21 +02:00
len
ac15c0c57e Fix an error when restoring backup 2016-03-31 12:17:40 +02:00
len
0c0372dc51 Fix #236 2016-03-30 18:17:00 +02:00
len
723c0e99a5 Remove unneeded class 2016-03-30 17:19:02 +02:00
len
e04596c668 Minor UI fixes 2016-03-30 10:30:13 +02:00
a809b05808 Merge pull request #212 from inorichi/backup
Support backups
2016-03-29 20:54:15 +02:00
da44dc3fb5 Support backups 2016-03-29 20:48:28 +02:00
len
06c63f1207 Fix a crash in the reader when restoring the instance. Removed capitalization on each word 2016-03-29 13:47:32 +02:00
c3425346b7 Merge pull request #234 from NoodleMage/sort
fix #99
2016-03-29 10:28:49 +02:00
fe8e4a4f54 fix #99 2016-03-28 14:07:24 +02:00
len
abab778e2e Try with a bigger heap to avoid OOM crashes 2016-03-26 13:28:22 +01:00
03ecb2fe13 Merge pull request #227 from NoodleMage/issue_42
Fix Issue #42
2016-03-26 13:14:16 +01:00
a78f89d4eb Can now choose to automatically remove chapter after reading (or previous). Fix #42 2016-03-25 23:05:50 +01:00
499c2213ee Implements delete chapter when set as read for issue #42 2016-03-25 23:05:46 +01:00
abbc7b572a Implements Download next 1/5/10/all chapters for issue #42 2016-03-25 23:05:45 +01:00
len
bf1fdda651 Fix crashes on settings 2016-03-25 15:50:25 +01:00
len
218ea7267d Remove lambdas 2016-03-23 20:28:44 +01:00
70cf085df9 Merge pull request #225 from NoodleMage/manga_chapters
Updated manga chapters UI
2016-03-23 19:51:20 +01:00
71783657af Merge pull request #201 from na-ji/master
Implement parser for readmanga.today
2016-03-23 19:45:29 +01:00
137c21e6c9 Added animation 2016-03-23 19:38:12 +01:00
bc473055b9 Changed fragment_manga_chapters.xml. Fix #221 2016-03-22 20:56:19 +01:00
3d67607768 Merge remote-tracking branch 'upstream/master' 2016-03-22 20:02:10 +01:00
len
ce271649ac Page number indicator now transparent 2016-03-21 15:58:59 +01:00
len
0078cb88c3 A few crashes fixed 2016-03-21 14:50:02 +01:00
len
19cb548e18 Fix last commit 2016-03-20 23:55:44 +01:00
len
b3cf7dbc14 Implement #226 2016-03-20 22:17:23 +01:00
len
bbfe0a0cd1 Fix directory picker 2016-03-20 21:50:48 +01:00
len
92b3f90380 Fix a query 2016-03-20 01:37:04 +01:00
len
b09345f2e1 Downgrade RxJava for a while 2016-03-19 23:08:47 +01:00
len
0d41c60a38 Fix tests 2016-03-19 21:09:51 +01:00
len
5132f4850f Minor changes 2016-03-19 19:30:55 +01:00
len
53fae2939a Remove apt, add manual EventBusIndex (not sure if it works) 2016-03-19 18:39:18 +01:00
len
14f248546a Use kapt, remove retrolambda, migrate database and source to Kotlin 2016-03-19 17:48:55 +01:00
len
0d519b3d16 Reader presenter in Kotlin + remove Icepick 2016-03-19 15:14:51 +01:00
len
8e0a9d6d66 Fix crashes 2016-03-19 01:03:28 +01:00
len
b8bc3476f4 Fix last commit 2016-03-19 00:22:13 +01:00
len
6b326cfb79 All events in Kotlin 2016-03-19 00:16:27 +01:00
len
aaef738dda Download manager in Kotlin and fix another crash in reader 2016-03-19 00:11:34 +01:00
len
35748fc1f3 Raw queries in Kotlin 2016-03-18 23:23:56 +01:00
len
a122d817e8 Fix ACRA not attaching BuildConfig 2016-03-18 20:40:14 +01:00
len
4ccce424de Reader view in Kotlin. Upgrade gradle wrapper. Remove ButterKnife from the project 2016-03-18 20:21:30 +01:00
len
396a79899e Bump dependencies. Fix crash in reader 2016-03-17 21:02:40 +01:00
len
de53681d2b Fix reader theme 2016-03-17 20:35:45 +01:00
aacd42b9f6 Merge pull request #218 from NoodleMage/theme_update
Rewrote Nav Drawer to Kotlin + Dark Theme
2016-03-17 20:34:29 +01:00
5ef5f9b45f Rewrote Theme 2016-03-17 20:01:52 +01:00
98d420d5aa Rewrote nav drawer to Kotlin + UI updates
Added launch screen + new Header

Removed MaterialDrawer library. Implemented Nav Draw from Support Library
2016-03-17 20:01:33 +01:00
len
5b75818fc5 Different approach for #214 2016-03-16 19:52:19 +01:00
len
f49577bc77 Manga in Kotlin. Expect some errors yet 2016-03-13 22:06:32 +01:00
len
a87c65872c Fix login dialogs not showing the correct title 2016-03-11 16:16:56 +01:00
ed636d5e2f Merge pull request #171 from Taumer/ru_parsers
Implement parsers for Readmanga, Mintmanga and Mangachan
2016-03-11 13:57:11 +01:00
2ced70652b Implement parsers for Readmanga, Mintmanga and Mangachan 2016-03-10 23:59:22 +03:00
dc742f4b8d Merge pull request #209 from inorichi/source-languages
Support for sources from different languages
2016-03-10 16:23:09 +01:00
len
689f2e7fbf Support for sources from different languages 2016-03-10 16:15:48 +01:00
len
ba1dca1826 Fix #206 2016-03-10 16:05:26 +01:00
len
a07e4c69b6 Fix crashes with vector drawables on older Android versions 2016-03-08 17:39:59 +01:00
len
05adde552d Kotlinize some widgets 2016-03-08 01:22:56 +01:00
0ddbfd1036 Merge pull request #204 from NoodleMage/svg_all_the_way
Converted all icon drawables to vector + Removed Android-Iconics library.
2016-03-08 00:47:26 +01:00
8b45df37d2 Converted all icon drawables to vector.
Removed Android-Iconics library.
2016-03-08 00:35:29 +01:00
len
70e557575f Preferences ported to support library 2016-03-07 23:48:43 +01:00
bcbd541d48 Merge pull request #198 from NoodleMage/issue_27
Added filter options. fix for Issue #27
2016-03-07 14:59:28 +01:00
6383a745ff Fixed wrong download filter from commit #33386e2
Fixed another tab not in TabLayout error.

Drawable to Vector
Removed Filter... Toast
2016-03-06 22:27:30 +01:00
len
b89d6644d8 Performance improvements for library filters 2016-03-06 22:27:26 +01:00
8fbef4b4bb Can now filter unread manga + Code opt 2016-03-06 22:27:24 +01:00
d9f5a97d56 Can now filter downloads only on library view. Fix #27 2016-03-06 22:27:22 +01:00
len
e4ee03cb61 Allow to cancel update. #192. Needs testing 2016-03-06 20:58:15 +01:00
c2a65c71e1 Merge pull request #202 from beschoenen/patch-1
Hide clear button
2016-03-06 18:20:42 +01:00
len
8c456a2da4 Replace some image drawables with vector drawables 2016-03-06 18:18:09 +01:00
f856386bf7 Hide clear button
Hide the clear butten when all downloads have finished.
2016-03-06 17:51:25 +01:00
e1448eaeda Merge pull request #199 from j2ghz/patch-1
Prevent issues with bad formatting like #194 #196
2016-03-05 20:52:21 +01:00
3cf61f2f93 Update ISSUE_TEMPLATE.md 2016-03-05 15:10:11 +01:00
len
ff61282104 Readers in Kotlin. Also fix #193 2016-03-04 14:10:41 +01:00
len
b2fe9d7d4d Fix #196 2016-03-03 20:16:28 +01:00
len
16a5e6c01a Some base classes and preferences in Kotlin 2016-03-02 16:17:56 +01:00
306b1d74bb Merge pull request #190 from NoodleMage/kotlin
Rewrote Recent to Kotlin
2016-03-02 15:30:59 +01:00
a7e652f1f7 Rewrote Recent to Kotlin 2016-03-02 14:54:51 +01:00
len
e1a3ab2726 Readded chapters do not notify. Fix #188 2016-03-02 14:18:50 +01:00
len
ae9c412b6d Fix possible crashes similar to #191 2016-03-02 14:07:54 +01:00
len
fad0027e17 Fix #191 2016-03-02 13:37:54 +01:00
len
1d7f012fd1 Fix builds 2016-03-02 03:15:36 +01:00
len
ee4bf163ef Catalogue in Kotlin. Support library upgraded to 23.2.0. Downloads directory now shows a list of folders, it should fix #141. 2016-03-01 23:29:07 +01:00
fabdba4452 Settings in Kotlin 2016-02-27 17:49:22 +01:00
1a14fc5c48 Merge pull request #183 from NoodleMage/master
Rewrote IOHandler to Kotlin
2016-02-27 14:13:21 +01:00
fa5b64ce2e Rewrote IOHandler to Kotlin 2016-02-27 10:35:14 +01:00
4397a44b80 Fix broken tests after last commit 2016-02-26 18:34:44 +01:00
d4bb092543 Allow custom parsing of chapter number on sources 2016-02-26 18:29:08 +01:00
f73f0cc341 Download queue's UI in Kotlin 2016-02-26 18:10:13 +01:00
b95d0e2848 Bump dependencies. Move ReactiveNetwork to app module. 2016-02-26 15:56:56 +01:00
50b97fa28f Log message error when a request from the catalogue fails. 2016-02-25 16:33:30 +01:00
61c7feca87 Refresh adapter after the cover is changed. Some minor changes on categories. 2016-02-25 16:13:48 +01:00
4dde6d1a31 Merge pull request #182 from NoodleMage/cover_edit_fix
Cover change fix (hopefully :-)
2016-02-25 16:11:17 +01:00
9ac2f02885 Cover change fix (hopefully :-) 2016-02-25 16:02:58 +01:00
19eb77f049 Merge pull request #180 from inorichi/library-kotlin
Migrate UI library to Kotlin.
2016-02-25 15:42:31 +01:00
59276b7160 Migrate library to Kotlin. 2016-02-25 15:37:52 +01:00
9062e40ec5 Merge pull request #178 from NoodleMage/kotlin
Category rewrite + FAB rewrite to Kotlin
2016-02-25 13:27:21 +01:00
9f78c8f6b4 - Rewrote Category to Kotlin
- Moved category to ui
- Reworked Animation (smoother)
- Updated TextDrawable
2016-02-25 09:58:01 +01:00
144d315e27 rewrote ScrollAwareFABBehavior.java to Kotlin. Can now implement FABAnimationBase to create different FAB animations 2016-02-24 11:57:57 +01:00
34dc85e605 Allow to retry image when decoding fails or open in the browser. Fixes #177 and fixes #120. Also fix a bug where the current page was not restored when changing settings. 2016-02-23 20:02:49 +01:00
4ba0f343e3 Fix #179 2016-02-23 17:32:47 +01:00
4876eaafcc Fix #168 and fix #81. 2016-02-22 16:53:33 +01:00
16f6be3613 Probable fix for #168. Maybe #81 also. Needs confirmation. 2016-02-22 14:59:57 +01:00
da0c7d7484 Merge pull request #175 from j2ghz/patch-1
Add link to wiki about debug F-Droid
2016-02-22 12:45:50 +01:00
fbd2f86f00 Add link to wiki about debug F-Droid 2016-02-22 09:29:51 +01:00
a309b7a066 Remove problematic test 2016-02-21 23:21:53 +01:00
9ad9c275a4 Delete an old file 2016-02-21 23:14:32 +01:00
7a467d43df Fix tests after Kotlin merge (probably) 2016-02-21 23:13:22 +01:00
db97250db8 Merge pull request #169 from inorichi/kotlin
Partial migration of data package to Kotlin
2016-02-21 22:42:54 +01:00
c0452038f7 Partial migration of data package to Kotlin 2016-02-21 18:55:37 +01:00
691 changed files with 18678 additions and 18578 deletions

View File

@ -1,6 +1,7 @@
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
Remove line above and describe your issue here. Fill out version below.
Remove line above and describe your issue here. Fill out version below. Use Preview.
---
Version: r000 or v0.0.0
(other relevant info like OS)

View File

@ -5,7 +5,7 @@ android:
- tools
# The BuildTools version used by your project
- build-tools-23.0.1
- build-tools-23.0.3
- android-23
- extra-android-m2repository
- extra-google-m2repository
@ -15,5 +15,14 @@ android:
before_script:
- chmod +x gradlew
#Build, and run tests
script: "./gradlew build testDebug"
script: "./gradlew clean assembleDebug testDebugUnitTest"
sudo: false
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
env:
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"

View File

@ -1,6 +1,6 @@
[![stable release](https://img.shields.io/badge/release-v0.1.4-blue.svg)](https://github.com/inorichi/tachiyomi/releases)
[![fdroid release](https://img.shields.io/badge/release-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi)
[![latest debug build](https://img.shields.io/badge/debug-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk)
| 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/badge/stable-v0.2.0-blue.svg)](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)
@ -10,7 +10,7 @@ Tachiyomi is a free and open source manga reader for Android.
Keep in mind it's still a beta, so expect it to crash sometimes.
## Features
# Features
* Online and offline reading
* Configurable reader with multiple viewers and settings

View File

@ -1,12 +1,8 @@
import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'me.tatarka.retrolambda'
retrolambda {
jvmArgs '-noverify'
}
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
ext {
// Git is needed in your system PATH for these commands to work.
@ -29,12 +25,12 @@ ext {
}
def includeUpdater() {
return hasProperty("include_updater");
return hasProperty("include_updater")
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
buildToolsVersion "23.0.3"
publishNonDefault true
defaultConfig {
@ -42,22 +38,20 @@ android {
minSdkVersion 16
targetSdkVersion 23
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 5
versionName "0.1.4"
versionCode 6
versionName "0.2.0"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
vectorDrawables.useSupportLibrary = true
}
buildTypes {
debug {
versionNameSuffix ".${getCommitCount()}"
applicationIdSuffix ".debug"
}
release {
@ -80,27 +74,32 @@ android {
checkReleaseBuilds false
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
// http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric
useLibrary 'org.apache.http.legacy'
}
apt {
arguments {
eventBusIndex "eu.kanade.tachiyomi.EventBusIndex"
}
kapt {
generateStubs = true
}
dependencies {
final SUPPORT_LIBRARY_VERSION = '23.1.1'
final DAGGER_VERSION = '2.0.2'
final EVENTBUS_VERSION = '3.0.0'
final OKHTTP_VERSION = '3.1.1'
final SUPPORT_LIBRARY_VERSION = '23.3.0'
final DAGGER_VERSION = '2.2'
final OKHTTP_VERSION = '3.2.0'
final RETROFIT_VERSION = '2.0.1'
final STORIO_VERSION = '1.8.0'
final ICEPICK_VERSION = '3.1.0'
final MOCKITO_VERSION = '1.10.19'
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(":SubsamplingScaleImageView")
compile project(":ReactiveNetwork")
// 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"
@ -108,57 +107,89 @@ dependencies {
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
// ReactiveX
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.1'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
// Network client
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
compile 'com.squareup.okio:okio:1.6.0'
compile 'com.google.code.gson:gson:2.5'
// REST
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.7.0'
// JSON
compile 'com.google.code.gson:gson:2.6.2'
// Disk cache
compile 'com.jakewharton:disklrucache:2.0.2'
// Parse HTML
compile 'org.jsoup:jsoup:1.8.3'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'
compile 'com.squareup.retrofit:retrofit:1.9.0'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
// Database
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
compile 'info.android15.nucleus:nucleus:2.0.4'
compile 'com.github.bumptech.glide:glide:3.6.1'
compile 'com.jakewharton:butterknife:7.0.1'
compile 'com.jakewharton.timber:timber:4.1.0'
compile 'ch.acra:acra:4.8.1'
compile "frankiesardo:icepick:$ICEPICK_VERSION"
provided "frankiesardo:icepick-processor:$ICEPICK_VERSION"
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.1'
compile 'com.github.amulyakhare:TextDrawable:558677e'
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
compile "org.greenrobot:eventbus:$EVENTBUS_VERSION"
apt "org.greenrobot:eventbus-annotation-processor:$EVENTBUS_VERSION"
// Model View Presenter
compile 'info.android15.nucleus:nucleus:2.0.5'
// Dependency injection
compile "com.google.dagger:dagger:$DAGGER_VERSION"
apt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
apt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
provided 'org.glassfish:javax.annotation:10.0-b28'
compile('com.mikepenz:materialdrawer:4.6.4@aar') {
// Image library
compile 'com.github.bumptech.glide:glide:3.7.0'
// Logging
compile 'com.jakewharton.timber:timber:4.1.2'
// Crash reports
compile 'ch.acra:acra:4.8.5'
// UI
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.github.afollestad.material-dialogs:core:0.8.5.5@aar') {
transitive = true
}
// Google material icons SVG.
compile 'com.mikepenz:google-material-typeface:2.1.0.1.original@aar'
compile('com.github.afollestad.material-dialogs:core:0.8.5.4@aar') {
transitive = true
}
// Tests
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:2.3.0'
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'
}
androidTestApt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
buildscript {
ext.kotlin_version = '1.0.1'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
repositories {
mavenCentral()
}

View File

@ -2,9 +2,6 @@
-keep class eu.kanade.tachiyomi.injection.** { *; }
# Retrolambda
-dontwarn java.lang.invoke.*
# OkHttp
-keepattributes Signature
-keepattributes *Annotation*
@ -19,38 +16,6 @@
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# ButterKnife 7
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keepclasseswithmembernames class * {
@butterknife.* <fields>;
}
-keepclasseswithmembernames class * {
@butterknife.* <methods>;
}
#Easy-Adapter v1.5.0
-keepattributes *Annotation*
-keepclassmembers class * extends uk.co.ribot.easyadapter.ItemViewHolder {
public <init>(...);
}
## GreenRobot EventBus specific rules ##
# http://greenrobot.org/eventbus/documentation/proguard/
-keepattributes *Annotation*
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
<init>(java.lang.Throwable);
}
# Glide specific rules #
# https://github.com/bumptech/glide
-keep public class * implements com.bumptech.glide.module.GlideModule
@ -60,7 +25,6 @@
}
# RxJava 1.1.0
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
@ -76,26 +40,17 @@
rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
# Retrofit 1.X
# Retrofit 2.X
## https://square.github.io/retrofit/ ##
-keep class com.squareup.okhttp.** { *; }
-keep class retrofit.** { *; }
-keep interface com.squareup.okhttp.** { *; }
-dontwarn com.squareup.okhttp.**
-dontwarn okio.**
-dontwarn retrofit.**
-dontwarn rx.**
-keepclasseswithmembers class * {
@retrofit.http.* <methods>;
}
# If in your rest service interface you use methods with Callback argument.
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
# If your rest service methods throw custom exceptions, because you've defined an ErrorHandler.
-keepattributes Signature
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# AppCombat
-keep public class android.support.v7.widget.** { *; }
@ -106,13 +61,6 @@
public <init>(android.content.Context);
}
# Icepick
-dontwarn icepick.**
-keep class **$$Icepick { *; }
-keepclasseswithmembernames class * {
@icepick.* <fields>;
}
## GSON 2.2.4 specific rules ##
# Gson uses generic type information stored in a class file when working with fields. Proguard

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi" >
<manifest package="eu.kanade.tachiyomi"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
@ -11,12 +12,14 @@
<application
android:name=".App"
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme" >
android:largeHeap="true"
android:theme="@style/Theme.Tachiyomi" >
<activity
android:name=".ui.main.MainActivity">
android:name=".ui.main.MainActivity"
android:theme="@style/Theme.BrandedLaunch">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -29,10 +32,7 @@
</activity>
<activity
android:name=".ui.reader.ReaderActivity"
android:parentActivityName=".ui.manga.MangaActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.manga.MangaActivity" />
android:theme="@style/Theme.Reader">
</activity>
<activity
android:name=".ui.setting.SettingsActivity"
@ -40,28 +40,27 @@
android:parentActivityName=".ui.main.MainActivity" >
</activity>
<activity
android:name=".ui.library.category.CategoryActivity"
android:name=".ui.category.CategoryActivity"
android:label="@string/label_categories"
android:parentActivityName=".ui.main.MainActivity">
</activity>
<activity
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme">
</activity>
<service android:name=".data.sync.LibraryUpdateService"
<service android:name=".data.library.LibraryUpdateService"
android:exported="false"/>
<service android:name=".data.download.DownloadService"
android:exported="false"/>
<service android:name=".data.sync.UpdateMangaSyncService"
<service android:name=".data.mangasync.UpdateMangaSyncService"
android:exported="false"/>
<receiver
android:name=".data.sync.LibraryUpdateService$SyncOnConnectionAvailable"
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
android:enabled="false">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
@ -69,7 +68,11 @@
</receiver>
<receiver
android:name=".data.sync.LibraryUpdateAlarm">
android:name=".data.library.LibraryUpdateService$LibraryUpdateReceiver">
</receiver>
<receiver
android:name=".data.library.LibraryUpdateAlarm">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="eu.kanade.UPDATE_LIBRARY" />

View File

@ -1,67 +0,0 @@
package eu.kanade.tachiyomi;
import android.app.Application;
import android.content.Context;
import org.acra.ACRA;
import org.acra.annotation.ReportsCrashes;
import org.greenrobot.eventbus.EventBus;
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector;
import eu.kanade.tachiyomi.injection.component.AppComponent;
import eu.kanade.tachiyomi.injection.component.DaggerAppComponent;
import eu.kanade.tachiyomi.injection.module.AppModule;
import timber.log.Timber;
@ReportsCrashes(
formUri = "http://tachiyomi.kanade.eu/crash_report",
reportType = org.acra.sender.HttpSender.Type.JSON,
httpMethod = org.acra.sender.HttpSender.Method.PUT,
excludeMatchingSharedPreferencesKeys={".*username.*",".*password.*"}
)
public class App extends Application {
AppComponent applicationComponent;
ComponentReflectionInjector<AppComponent> componentInjector;
public static App get(Context context) {
return (App) context.getApplicationContext();
}
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree());
applicationComponent = DaggerAppComponent.builder()
.appModule(new AppModule(this))
.build();
componentInjector =
new ComponentReflectionInjector<>(AppComponent.class, applicationComponent);
setupEventBus();
ACRA.init(this);
}
protected void setupEventBus() {
EventBus.builder()
.addIndex(new EventBusIndex())
.logNoSubscriberMessages(false)
.installDefaultEventBus();
}
public AppComponent getComponent() {
return applicationComponent;
}
// Needed to replace the component with a test specific one
public void setComponent(AppComponent applicationComponent) {
this.applicationComponent = applicationComponent;
}
public ComponentReflectionInjector<AppComponent> getComponentReflection() {
return componentInjector;
}
}

View File

@ -0,0 +1,62 @@
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
@ReportsCrashes(
formUri = "http://tachiyomi.kanade.eu/crash_report",
reportType = org.acra.sender.HttpSender.Type.JSON,
httpMethod = org.acra.sender.HttpSender.Method.PUT,
buildConfigClass = BuildConfig::class,
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*")
)
open class App : Application() {
lateinit var component: AppComponent
private set
lateinit var componentReflection: ComponentReflectionInjector<AppComponent>
private set
var appTheme = 0
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
component = prepareAppComponent().build()
componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
setupTheme()
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,381 @@
package eu.kanade.tachiyomi.data.backup
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.*
/**
* This class provides the necessary methods to create and restore backups for the data of the
* application. The backup follows a JSON structure, with the following scheme:
*
* {
* "mangas": [
* {
* "manga": {"id": 1, ...},
* "chapters": [{"id": 1, ...}, {...}],
* "sync": [{"id": 1, ...}, {...}],
* "categories": ["cat1", "cat2", ...]
* },
* { ... }
* ],
* "categories": [
* {"id": 1, ...},
* {"id": 2, ...}
* ]
* }
*
* @param db the database helper.
*/
class BackupManager(private val db: DatabaseHelper) {
private val MANGA = "manga"
private val MANGAS = "mangas"
private val CHAPTERS = "chapters"
private val MANGA_SYNC = "sync"
private val CATEGORIES = "categories"
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private val gson = GsonBuilder()
.registerTypeAdapter(Integer::class.java, IntegerSerializer())
.setExclusionStrategies(IdExclusion())
.create()
/**
* Backups the data of the application to a file.
*
* @param file the file where the backup will be saved.
* @throws IOException if there's any IO error.
*/
@Throws(IOException::class)
fun backupToFile(file: File) {
val root = backupToJson()
FileWriter(file).use {
gson.toJson(root, it)
}
}
/**
* Creates a JSON object containing the backup of the app's data.
*
* @return the backup as a JSON object.
*/
fun backupToJson(): JsonObject {
val root = JsonObject()
// Backup library mangas and its dependencies
val mangaEntries = JsonArray()
root.add(MANGAS, mangaEntries)
for (manga in db.getFavoriteMangas().executeAsBlocking()) {
mangaEntries.add(backupManga(manga))
}
// Backup categories
val categoryEntries = JsonArray()
root.add(CATEGORIES, categoryEntries)
for (category in db.getCategories().executeAsBlocking()) {
categoryEntries.add(backupCategory(category))
}
return root
}
/**
* Backups a manga and its related data (chapters, categories this manga is in, sync...).
*
* @param manga the manga to backup.
* @return a JSON object containing all the data of the manga.
*/
private fun backupManga(manga: Manga): JsonObject {
// Entry for this manga
val entry = JsonObject()
// Backup manga fields
entry.add(MANGA, gson.toJsonTree(manga))
// Backup all the chapters
val chapters = db.getChapters(manga).executeAsBlocking()
if (!chapters.isEmpty()) {
entry.add(CHAPTERS, gson.toJsonTree(chapters))
}
// Backup manga sync
val mangaSync = db.getMangasSync(manga).executeAsBlocking()
if (!mangaSync.isEmpty()) {
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync))
}
// Backup categories for this manga
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
if (!categoriesForManga.isEmpty()) {
val categoriesNames = ArrayList<String>()
for (category in categoriesForManga) {
categoriesNames.add(category.name)
}
entry.add(CATEGORIES, gson.toJsonTree(categoriesNames))
}
return entry
}
/**
* Backups a category.
*
* @param category the category to backup.
* @return a JSON object containing the data of the category.
*/
private fun backupCategory(category: Category): JsonElement {
return gson.toJsonTree(category)
}
/**
* Restores a backup from a file.
*
* @param file the file containing the backup.
* @throws IOException if there's any IO error.
*/
@Throws(IOException::class)
fun restoreFromFile(file: File) {
JsonReader(FileReader(file)).use {
val root = JsonParser().parse(it).asJsonObject
restoreFromJson(root)
}
}
/**
* Restores a backup from an input stream.
*
* @param stream the stream containing the backup.
* @throws IOException if there's any IO error.
*/
@Throws(IOException::class)
fun restoreFromStream(stream: InputStream) {
JsonReader(InputStreamReader(stream)).use {
val root = JsonParser().parse(it).asJsonObject
restoreFromJson(root)
}
}
/**
* Restores a backup from a JSON object. Everything executes in a single transaction so that
* nothing is modified if there's an error.
*
* @param root the root of the JSON.
*/
fun restoreFromJson(root: JsonObject) {
db.inTransaction {
// Restore categories
root.get(CATEGORIES)?.let {
restoreCategories(it.asJsonArray)
}
// Restore mangas
root.get(MANGAS)?.let {
restoreMangas(it.asJsonArray)
}
}
}
/**
* Restores the categories.
*
* @param jsonCategories the categories of the json.
*/
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)
// Iterate over them
for (category in backupCategories) {
// Used to know if the category is already in the db
var found = false
for (dbCategory in dbCategories) {
// If the category is already in the db, assign the id to the file's category
// and do nothing
if (category.nameLower == dbCategory.nameLower) {
category.id = dbCategory.id
found = true
break
}
}
// If the category isn't in the db, remove the id and insert a new category
// Store the inserted id in the category
if (!found) {
// Let the db assign the id
category.id = null
val result = db.insertCategory(category).executeAsBlocking()
category.id = result.insertedId()?.toInt()
}
}
}
/**
* Restores all the mangas and its related data.
*
* @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)
// Restore everything related to this manga
restoreManga(manga)
restoreChaptersForManga(manga, chapters)
restoreSyncForManga(manga, sync)
restoreCategoriesForManga(manga, categories)
}
}
/**
* Restores a manga.
*
* @param manga the manga to restore.
*/
private fun restoreManga(manga: Manga) {
// Try to find existing manga in db
val dbManga = db.getManga(manga.url, manga.source).executeAsBlocking()
if (dbManga == null) {
// Let the db assign the id
manga.id = null
val result = db.insertManga(manga).executeAsBlocking()
manga.id = result.insertedId()
} else {
// If it exists already, we copy only the values related to the source from the db
// (they can be up to date). Local values (flags) are kept from the backup.
manga.id = dbManga.id
manga.copyFrom(dbManga)
manga.favorite = true
db.insertManga(manga).executeAsBlocking()
}
}
/**
* Restores the chapters of a manga.
*
* @param manga the manga whose chapters have to be restored.
* @param chapters the chapters to restore.
*/
private fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
// Fix foreign keys with the current manga id
for (chapter in chapters) {
chapter.manga_id = manga.id
}
val dbChapters = db.getChapters(manga).executeAsBlocking()
val chaptersToUpdate = ArrayList<Chapter>()
for (backupChapter in chapters) {
// Try to find existing chapter in db
val pos = dbChapters.indexOf(backupChapter)
if (pos != -1) {
// The chapter is already in the db, only update its fields
val dbChapter = dbChapters[pos]
// If one of them was read, the chapter will be marked as read
dbChapter.read = backupChapter.read || dbChapter.read
dbChapter.last_page_read = Math.max(backupChapter.last_page_read, dbChapter.last_page_read)
chaptersToUpdate.add(dbChapter)
} else {
// Insert new chapter. Let the db assign the id
backupChapter.id = null
chaptersToUpdate.add(backupChapter)
}
}
// Update database
if (!chaptersToUpdate.isEmpty()) {
db.insertChapters(chaptersToUpdate).executeAsBlocking()
}
}
/**
* Restores the categories a manga is in.
*
* @param manga the manga whose categories have to be restored.
* @param categories the categories to restore.
*/
private fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
val dbCategories = db.getCategories().executeAsBlocking()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
for (backupCategoryStr in categories) {
for (dbCategory in dbCategories) {
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
break
}
}
}
// Update database
if (!mangaCategoriesToUpdate.isEmpty()) {
val mangaAsList = ArrayList<Manga>()
mangaAsList.add(manga)
db.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
}
}
/**
* Restores the sync of a manga.
*
* @param manga the manga whose sync have to be restored.
* @param sync the sync to restore.
*/
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
}
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
val syncToUpdate = ArrayList<MangaSync>()
for (backupSync in sync) {
// Try to find existing chapter in db
val pos = dbSyncs.indexOf(backupSync)
if (pos != -1) {
// The sync is already in the db, only update its fields
val dbSync = dbSyncs[pos]
// Mark the max chapter as read and nothing else
dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read)
syncToUpdate.add(dbSync)
} else {
// Insert new sync. Let the db assign the id
backupSync.id = null
syncToUpdate.add(backupSync)
}
}
// Update database
if (!syncToUpdate.isEmpty()) {
db.insertMangasSync(syncToUpdate).executeAsBlocking()
}
}
/**
* 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

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync
class IdExclusion : ExclusionStrategy {
private val categoryExclusions = listOf("id")
private val mangaExclusions = listOf("id")
private val chapterExclusions = listOf("id", "manga_id")
private val syncExclusions = listOf("id", "manga_id", "update")
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
Manga::class.java -> mangaExclusions.contains(f.name)
Chapter::class.java -> chapterExclusions.contains(f.name)
MangaSync::class.java -> syncExclusions.contains(f.name)
Category::class.java -> categoryExclusions.contains(f.name)
else -> false
}
override fun shouldSkipClass(clazz: Class<*>) = false
}

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
class IntegerSerializer : JsonSerializer<Int> {
override fun serialize(value: Int?, type: Type, context: JsonSerializationContext): JsonElement? {
if (value != null && value !== 0)
return JsonPrimitive(value)
return null
}
}

View File

@ -1,268 +0,0 @@
package eu.kanade.tachiyomi.data.cache;
import android.content.Context;
import android.text.format.Formatter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.jakewharton.disklrucache.DiskLruCache;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.List;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.DiskUtils;
import okhttp3.Response;
import okio.BufferedSink;
import okio.Okio;
import rx.Observable;
/**
* Class used to create chapter cache
* For each image in a chapter a file is created
* For each chapter a Json list is created and converted to a file.
* The files are in format *md5key*.0
*/
public class ChapterCache {
/** Name of cache directory. */
private static final String PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache";
/** Application cache version. */
private static final int PARAMETER_APP_VERSION = 1;
/** The number of values per cache entry. Must be positive. */
private static final int PARAMETER_VALUE_COUNT = 1;
/** The maximum number of bytes this cache should use to store. */
private static final int PARAMETER_CACHE_SIZE = 75 * 1024 * 1024;
/** Interface to global information about an application environment. */
private final Context context;
/** Google Json class used for parsing JSON files. */
private final Gson gson;
/** Cache class used for cache management. */
private DiskLruCache diskCache;
/** Page list collection used for deserializing from JSON. */
private final Type pageListCollection;
/**
* Constructor of ChapterCache.
* @param context application environment interface.
*/
public ChapterCache(Context context) {
this.context = context;
// Initialize Json handler.
gson = new Gson();
// Try to open cache in default cache directory.
try {
diskCache = DiskLruCache.open(
new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE
);
} catch (IOException e) {
// Do Nothing.
}
pageListCollection = new TypeToken<List<Page>>() {}.getType();
}
/**
* Returns directory of cache.
* @return directory of cache.
*/
public File getCacheDir() {
return diskCache.getDirectory();
}
/**
* Returns real size of directory.
* @return real size of directory.
*/
private long getRealSize() {
return DiskUtils.getDirectorySize(getCacheDir());
}
/**
* Returns real size of directory in human readable format.
* @return real size of directory.
*/
public String getReadableSize() {
return Formatter.formatFileSize(context, getRealSize());
}
/**
* Remove file from cache.
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
public boolean removeFileFromCache(String file) {
// Make sure we don't delete the journal file (keeps track of cache).
if (file.equals("journal") || file.startsWith("journal."))
return false;
try {
// Remove the extension from the file to get the key of the cache
String key = file.substring(0, file.lastIndexOf("."));
// Remove file from cache.
return diskCache.remove(key);
} catch (IOException e) {
return false;
}
}
/**
* Get page list from cache.
* @param chapterUrl the url of the chapter.
* @return an observable of the list of pages.
*/
public Observable<List<Page>> getPageListFromCache(final String chapterUrl) {
return Observable.fromCallable(() -> {
// Initialize snapshot (a snapshot of the values for an entry).
DiskLruCache.Snapshot snapshot = null;
try {
// Create md5 key and retrieve snapshot.
String key = DiskUtils.hashKeyForDisk(chapterUrl);
snapshot = diskCache.get(key);
// Convert JSON string to list of objects.
return gson.fromJson(snapshot.getString(0), pageListCollection);
} finally {
if (snapshot != null) {
snapshot.close();
}
}
});
}
/**
* Add page list to disk cache.
* @param chapterUrl the url of the chapter.
* @param pages list of pages.
*/
public void putPageListToCache(final String chapterUrl, final List<Page> pages) {
// Convert list of pages to json string.
String cachedValue = gson.toJson(pages);
// Initialize the editor (edits the values for an entry).
DiskLruCache.Editor editor = null;
// Initialize OutputStream.
OutputStream outputStream = null;
try {
// Get editor from md5 key.
String key = DiskUtils.hashKeyForDisk(chapterUrl);
editor = diskCache.edit(key);
if (editor == null) {
return;
}
// Write chapter urls to cache.
outputStream = new BufferedOutputStream(editor.newOutputStream(0));
outputStream.write(cachedValue.getBytes());
outputStream.flush();
diskCache.flush();
editor.commit();
} catch (Exception e) {
// Do Nothing.
} finally {
if (editor != null) {
editor.abortUnlessCommitted();
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException ignore) {
// Do Nothing.
}
}
}
}
/**
* Check if image is in cache.
* @param imageUrl url of image.
* @return true if in cache otherwise false.
*/
public boolean isImageInCache(final String imageUrl) {
try {
return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null;
} catch (IOException e) {
return false;
}
}
/**
* Get image path from url.
* @param imageUrl url of image.
* @return path of image.
*/
public String getImagePath(final String imageUrl) {
try {
// Get file from md5 key.
String imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0";
File file = new File(diskCache.getDirectory(), imageName);
return file.getCanonicalPath();
} catch (IOException e) {
return null;
}
}
/**
* Add image to cache.
* @param imageUrl url of image.
* @param response http response from page.
* @throws IOException image error.
*/
public void putImageToCache(final String imageUrl, final Response response) throws IOException {
// Initialize editor (edits the values for an entry).
DiskLruCache.Editor editor = null;
// Initialize BufferedSink (used for small writes).
BufferedSink sink = null;
try {
// Get editor from md5 key.
String key = DiskUtils.hashKeyForDisk(imageUrl);
editor = diskCache.edit(key);
if (editor == null) {
throw new IOException("Unable to edit key");
}
// Initialize OutputStream and write image.
OutputStream outputStream = new BufferedOutputStream(editor.newOutputStream(0));
sink = Okio.buffer(Okio.sink(outputStream));
sink.writeAll(response.body().source());
diskCache.flush();
editor.commit();
} catch (Exception e) {
response.body().close();
throw new IOException("Unable to save image");
} finally {
if (editor != null) {
editor.abortUnlessCommitted();
}
if (sink != null) {
sink.close();
}
}
}
}

View File

@ -0,0 +1,213 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import android.text.format.Formatter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtils
import okhttp3.Response
import okio.Okio
import rx.Observable
import java.io.File
import java.io.IOException
import java.lang.reflect.Type
/**
* Class used to create chapter cache
* For each image in a chapter a file is created
* For each chapter a Json list is created and converted to a file.
* The files are in format *md5key*.0
*
* @param context the application context.
* @constructor creates an instance of the chapter cache.
*/
class ChapterCache(private val context: Context) {
/** Google Json class used for parsing JSON files. */
private val gson: Gson = Gson()
/** Cache class used for cache management. */
private val diskCache: DiskLruCache
/** Page list collection used for deserializing from JSON. */
private val pageListCollection: Type = object : TypeToken<List<Page>>() {}.type
companion object {
/** Name of cache directory. */
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
/** Application cache version. */
const val PARAMETER_APP_VERSION = 1
/** The number of values per cache entry. Must be positive. */
const val PARAMETER_VALUE_COUNT = 1
/** The maximum number of bytes this cache should use to store. */
const val PARAMETER_CACHE_SIZE = 75L * 1024 * 1024
}
init {
// Open cache in default cache directory.
diskCache = DiskLruCache.open(
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE)
}
/**
* Returns directory of cache.
* @return directory of cache.
*/
val cacheDir: File
get() = diskCache.directory
/**
* Returns real size of directory.
* @return real size of directory.
*/
private val realSize: Long
get() = DiskUtils.getDirectorySize(cacheDir)
/**
* Returns real size of directory in human readable format.
* @return real size of directory.
*/
val readableSize: String
get() = Formatter.formatFileSize(context, realSize)
/**
* Remove file from cache.
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
if (file == "journal" || file.startsWith("journal."))
return false
try {
// Remove the extension from the file to get the key of the cache
val key = file.substring(0, file.lastIndexOf("."))
// Remove file from cache.
return diskCache.remove(key)
} catch (e: IOException) {
return false
}
}
/**
* Get page list from cache.
* @param chapterUrl the url of the chapter.
* @return an observable of the list of pages.
*/
fun getPageListFromCache(chapterUrl: String): Observable<List<Page>> {
return Observable.fromCallable<List<Page>> {
// Get the key for the chapter.
val key = DiskUtils.hashKeyForDisk(chapterUrl)
// Convert JSON string to list of objects. Throws an exception if snapshot is null
diskCache.get(key).use {
gson.fromJson(it.getString(0), pageListCollection)
}
}
}
/**
* Add page list to disk cache.
* @param chapterUrl the url of the chapter.
* @param pages list of pages.
*/
fun putPageListToCache(chapterUrl: String, pages: List<Page>) {
// Convert list of pages to json string.
val cachedValue = gson.toJson(pages)
// Initialize the editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null
try {
// Get editor from md5 key.
val key = DiskUtils.hashKeyForDisk(chapterUrl)
editor = diskCache.edit(key) ?: return
// Write chapter urls to cache.
Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
it.write(cachedValue.toByteArray())
it.flush()
}
diskCache.flush()
editor.commit()
editor.abortUnlessCommitted()
} catch (e: Exception) {
// Ignore.
} finally {
editor?.abortUnlessCommitted()
}
}
/**
* Check if image is in cache.
* @param imageUrl url of image.
* @return true if in cache otherwise false.
*/
fun isImageInCache(imageUrl: String): Boolean {
try {
return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null
} catch (e: IOException) {
return false
}
}
/**
* Get image path from url.
* @param imageUrl url of image.
* @return path of image.
*/
fun getImagePath(imageUrl: String): String? {
try {
// Get file from md5 key.
val imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0"
return File(diskCache.directory, imageName).canonicalPath
} catch (e: IOException) {
return null
}
}
/**
* Add image to cache.
* @param imageUrl url of image.
* @param response http response from page.
* @throws IOException image error.
*/
@Throws(IOException::class)
fun putImageToCache(imageUrl: String, response: Response) {
// Initialize editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null
try {
// Get editor from md5 key.
val key = DiskUtils.hashKeyForDisk(imageUrl)
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
// Get OutputStream and write image with Okio.
Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
it.writeAll(response.body().source())
it.flush()
}
diskCache.flush()
editor.commit()
} catch (e: Exception) {
response.body().close()
throw IOException("Unable to save image")
} finally {
editor?.abortUnlessCommitted()
}
}
}

View File

@ -1,235 +0,0 @@
package eu.kanade.tachiyomi.data.cache;
import android.content.Context;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.LazyHeaders;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.signature.StringSignature;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import eu.kanade.tachiyomi.util.DiskUtils;
/**
* Class used to create cover cache
* It is used to store the covers of the library.
* Makes use of Glide (which can avoid repeating requests) to download covers.
* Names of files are created with the md5 of the thumbnail URL
*/
public class CoverCache {
/**
* Name of cache directory.
*/
private static final String PARAMETER_CACHE_DIRECTORY = "cover_disk_cache";
/**
* Interface to global information about an application environment.
*/
private final Context context;
/**
* Cache directory used for cache management.
*/
private final File cacheDir;
/**
* Constructor of CoverCache.
*
* @param context application environment interface.
*/
public CoverCache(Context context) {
this.context = context;
// Get cache directory from parameter.
cacheDir = new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY);
// Create cache directory.
createCacheDir();
}
/**
* Create cache directory if it doesn't exist
*
* @return true if cache dir is created otherwise false.
*/
private boolean createCacheDir() {
return !cacheDir.exists() && cacheDir.mkdirs();
}
/**
* Download the cover with Glide and save the file in this cache.
*
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
*/
public void save(String thumbnailUrl, LazyHeaders headers) {
save(thumbnailUrl, headers, null);
}
/**
* Download the cover with Glide and save the file.
*
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
* @param imageView imageView where picture should be displayed.
*/
private void save(String thumbnailUrl, LazyHeaders headers, @Nullable ImageView imageView) {
// Check if url is empty.
if (TextUtils.isEmpty(thumbnailUrl))
return;
// Download the cover with Glide and save the file.
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
Glide.with(context)
.load(url)
.downloadOnly(new SimpleTarget<File>() {
@Override
public void onResourceReady(File resource, GlideAnimation<? super File> anim) {
try {
// Copy the cover from Glide's cache to local cache.
copyToLocalCache(thumbnailUrl, resource);
// Check if imageView isn't null and show picture in imageView.
if (imageView != null) {
loadFromCache(imageView, resource);
}
} catch (IOException e) {
// Do nothing.
}
}
});
}
/**
* Copy the cover from Glide's cache to this cache.
*
* @param thumbnailUrl url of thumbnail.
* @param source the cover image.
* @throws IOException exception returned
*/
public void copyToLocalCache(String thumbnailUrl, File source) throws IOException {
// Create cache directory if needed.
createCacheDir();
// Get destination file.
File dest = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
// Delete the current file if it exists.
if (dest.exists())
dest.delete();
// Write thumbnail image to file.
InputStream in = new FileInputStream(source);
try {
OutputStream out = new FileOutputStream(dest);
try {
// Transfer bytes from in to out.
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
/**
* Returns the cover from cache.
*
* @param thumbnailUrl the thumbnail url.
* @return cover image.
*/
private File getCoverFromCache(String thumbnailUrl) {
return new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
}
/**
* Delete the cover file from the cache.
*
* @param thumbnailUrl the thumbnail url.
* @return status of deletion.
*/
public boolean deleteCoverFromCache(String thumbnailUrl) {
// Check if url is empty.
if (TextUtils.isEmpty(thumbnailUrl))
return false;
// Remove file.
File file = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
return file.exists() && file.delete();
}
/**
* Save or load the image from cache
*
* @param imageView imageView where picture should be displayed.
* @param thumbnailUrl the thumbnail url.
* @param headers headers included in Glide request.
*/
public void saveOrLoadFromCache(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
// If file exist load it otherwise save it.
File localCover = getCoverFromCache(thumbnailUrl);
if (localCover.exists()) {
loadFromCache(imageView, localCover);
} else {
save(thumbnailUrl, headers, imageView);
}
}
/**
* Helper method to load the cover from the cache directory into the specified image view.
* Glide stores the resized image in its cache to improve performance.
*
* @param imageView imageView where picture should be displayed.
* @param file file to load. Must exist!.
*/
private void loadFromCache(ImageView imageView, File file) {
Glide.with(context)
.load(file)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.signature(new StringSignature(String.valueOf(file.lastModified())))
.into(imageView);
}
/**
* Helper method to load the cover from network into the specified image view.
* The source image is stored in Glide's cache so that it can be easily copied to this cache
* if the manga is added to the library.
*
* @param imageView imageView where picture should be displayed.
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
*/
public void loadFromNetwork(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
// Check if url is empty.
if (TextUtils.isEmpty(thumbnailUrl))
return;
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
Glide.with(context)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.centerCrop()
.into(imageView);
}
}

View File

@ -0,0 +1,131 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.animation.GlideAnimation
import com.bumptech.glide.request.target.SimpleTarget
import eu.kanade.tachiyomi.util.DiskUtils
import java.io.File
import java.io.IOException
import java.io.InputStream
/**
* Class used to create cover cache.
* It is used to store the covers of the library.
* Makes use of Glide (which can avoid repeating requests) to download covers.
* Names of files are created with the md5 of the thumbnail URL.
*
* @param context the application context.
* @constructor creates an instance of the cover cache.
*/
class CoverCache(private val context: Context) {
/**
* Cache directory used for cache management.
*/
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
/**
* Download the cover with Glide and save the file.
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
* @param onReady function to call when the image is ready
*/
fun save(thumbnailUrl: String?, headers: LazyHeaders, onReady: ((File) -> Unit)? = null) {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return
// Download the cover with Glide and save the file.
val url = GlideUrl(thumbnailUrl, headers)
Glide.with(context)
.load(url)
.downloadOnly(object : SimpleTarget<File>() {
override fun onResourceReady(resource: File, anim: GlideAnimation<in File>) {
try {
// Copy the cover from Glide's cache to local cache.
copyToCache(thumbnailUrl!!, resource)
onReady?.invoke(resource)
} catch (e: IOException) {
// Do nothing.
}
}
})
}
/**
* Save or load the image from cache
* @param thumbnailUrl the thumbnail url.
* @param headers headers included in Glide request.
* @param onReady function to call when the image is ready
*/
fun saveOrLoadFromCache(thumbnailUrl: String?, headers: LazyHeaders, onReady: ((File) -> Unit)?) {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return
// If file exist load it otherwise save it.
val localCover = getCoverFromCache(thumbnailUrl!!)
if (localCover.exists()) {
onReady?.invoke(localCover)
} else {
save(thumbnailUrl, headers, onReady)
}
}
/**
* Returns the cover from cache.
* @param thumbnailUrl the thumbnail url.
* @return cover image.
*/
private fun getCoverFromCache(thumbnailUrl: String): File {
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
}
/**
* Copy the given file to this cache.
* @param thumbnailUrl url of thumbnail.
* @param sourceFile the source file of the cover image.
* @throws IOException if there's any error.
*/
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, sourceFile: File) {
// Get destination file.
val destFile = getCoverFromCache(thumbnailUrl)
sourceFile.copyTo(destFile, overwrite = true)
}
/**
* Copy the given stream to this cache.
* @param thumbnailUrl url of the thumbnail.
* @param inputStream the stream to copy.
* @throws IOException if there's any error.
*/
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
// Get destination file.
val destFile = getCoverFromCache(thumbnailUrl)
destFile.outputStream().use { inputStream.copyTo(it) }
}
/**
* Delete the cover file from the cache.
* @param thumbnailUrl the thumbnail url.
* @return status of deletion.
*/
fun deleteFromCache(thumbnailUrl: String?): Boolean {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return false
// Remove file.
val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
return file.exists() && file.delete()
}
}

View File

@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.data.cache;
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
import com.bumptech.glide.module.GlideModule;
/**
* Class used to update Glide module settings
*/
public class CoverGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
// Bitmaps decoded from most image formats (other than GIFs with hidden configs)
// will be decoded with the ARGB_8888 config.
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
// Set the cache size of Glide to 15 MiB
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024));
}
@Override
public void registerComponents(Context context, Glide glide) {
// Nothing to see here!
}
}

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.module.GlideModule
/**
* Class used to update Glide module settings
*/
class CoverGlideModule : GlideModule {
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) {
// Nothing to see here!
}
}

View File

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

View File

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

View File

@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.data.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
public class DbOpenHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "tachiyomi.db";
public static final int DATABASE_VERSION = 1;
public DbOpenHelper(@NonNull Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(@NonNull SQLiteDatabase db) {
db.execSQL(MangaTable.getCreateTableQuery());
db.execSQL(ChapterTable.getCreateTableQuery());
db.execSQL(MangaSyncTable.getCreateTableQuery());
db.execSQL(CategoryTable.getCreateTableQuery());
db.execSQL(MangaCategoryTable.getCreateTableQuery());
// DB indexes
db.execSQL(MangaTable.getCreateUrlIndexQuery());
db.execSQL(MangaTable.getCreateFavoriteIndexQuery());
db.execSQL(ChapterTable.getCreateMangaIdIndexQuery());
}
@Override
public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
}
@Override
public void onConfigure(@NonNull SQLiteDatabase db) {
db.setForeignKeyConstraintsEnabled(true);
}
}

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.data.database
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import eu.kanade.tachiyomi.data.database.tables.*
class DbOpenHelper(context: Context) : SQLiteOpenHelper(context, DbOpenHelper.DATABASE_NAME, null, DbOpenHelper.DATABASE_VERSION) {
companion object {
/**
* Name of the database file.
*/
const val DATABASE_NAME = "tachiyomi.db"
/**
* Version of the database.
*/
const val DATABASE_VERSION = 1
}
override fun onCreate(db: SQLiteDatabase) = with(db) {
execSQL(MangaTable.getCreateTableQuery())
execSQL(ChapterTable.getCreateTableQuery())
execSQL(MangaSyncTable.getCreateTableQuery())
execSQL(CategoryTable.getCreateTableQuery())
execSQL(MangaCategoryTable.getCreateTableQuery())
// DB indexes
execSQL(MangaTable.getCreateUrlIndexQuery())
execSQL(MangaTable.getCreateFavoriteIndexQuery())
execSQL(ChapterTable.getCreateMangaIdIndexQuery())
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
}
override fun onConfigure(db: SQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)
}
}

View File

@ -0,0 +1,54 @@
package eu.kanade.tachiyomi.data.database
import java.util.*
import eu.kanade.tachiyomi.data.database.models.Manga as MangaModel
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
/**
* Query to get the manga from the library, with their categories and unread count.
*/
val libraryQuery =
"SELECT M.*, COALESCE(MC.${MangaCategory.COLUMN_CATEGORY_ID}, 0) AS ${Manga.COLUMN_CATEGORY} " +
"FROM (" +
"SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COLUMN_UNREAD} " +
"FROM ${Manga.TABLE} " +
"LEFT JOIN (" +
"SELECT ${Chapter.COLUMN_MANGA_ID}, COUNT(*) AS unread " +
"FROM ${Chapter.TABLE} " +
"WHERE ${Chapter.COLUMN_READ} = 0 " +
"GROUP BY ${Chapter.COLUMN_MANGA_ID}" +
") AS C " +
"ON ${Manga.COLUMN_ID} = C.${Chapter.COLUMN_MANGA_ID} " +
"WHERE ${Manga.COLUMN_FAVORITE} = 1 " +
"GROUP BY ${Manga.COLUMN_ID} " +
"ORDER BY ${Manga.COLUMN_TITLE}" +
") AS M " +
"LEFT JOIN (" +
"SELECT * FROM ${MangaCategory.TABLE}) AS MC " +
"ON MC.${MangaCategory.COLUMN_MANGA_ID} = M.${Manga.COLUMN_ID}"
/**
* Query to get the recent chapters of manga from the library up to a date.
*
* @param date the delimiting date.
*/
fun getRecentsQuery(date: Date): String =
"SELECT ${Manga.TABLE}.${Manga.COLUMN_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} " +
"ON ${Manga.TABLE}.${Manga.COLUMN_ID} = ${Chapter.TABLE}.${Chapter.COLUMN_MANGA_ID} " +
"WHERE ${Manga.COLUMN_FAVORITE} = 1 AND ${Chapter.COLUMN_DATE_UPLOAD} > ${date.time} " +
"ORDER BY ${Chapter.COLUMN_DATE_UPLOAD} DESC"
/**
* Query to get the categorias for a manga.
*
* @param manga the manga.
*/
fun getCategoriesForMangaQuery(manga: MangaModel) =
"SELECT ${Category.TABLE}.* FROM ${Category.TABLE} " +
"JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COLUMN_ID} = " +
"${MangaCategory.TABLE}.${MangaCategory.COLUMN_CATEGORY_ID} " +
"WHERE ${MangaCategory.COLUMN_MANGA_ID} = ${manga.id}"

View File

@ -35,4 +35,23 @@ public class Category implements Serializable {
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

@ -59,9 +59,9 @@ public class Manga implements Serializable {
@StorIOSQLiteColumn(name = MangaTable.COLUMN_CHAPTER_FLAGS)
public int chapter_flags;
public int unread;
public transient int unread;
public int category;
public transient int category;
public static final int UNKNOWN = 0;
public static final int ONGOING = 1;

View File

@ -40,6 +40,10 @@ public class MangaSync implements Serializable {
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();
@ -52,4 +56,23 @@ public class MangaSync implements Serializable {
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

@ -1,61 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers;
import android.database.Cursor;
import android.support.annotation.NonNull;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
public class LibraryMangaGetResolver extends MangaStorIOSQLiteGetResolver {
public static final LibraryMangaGetResolver INSTANCE = new LibraryMangaGetResolver();
public static final String QUERY = String.format(
"SELECT M.*, COALESCE(MC.%10$s, 0) AS %12$s " +
"FROM (" +
"SELECT %1$s.*, COALESCE(C.unread, 0) AS %6$s " +
"FROM %1$s " +
"LEFT JOIN (" +
"SELECT %5$s, COUNT(*) AS unread " +
"FROM %2$s " +
"WHERE %7$s = 0 " +
"GROUP BY %5$s" +
") AS C " +
"ON %4$s = C.%5$s " +
"WHERE %8$s = 1 " +
"GROUP BY %4$s " +
"ORDER BY %9$s" +
") AS M " +
"LEFT JOIN (SELECT * FROM %3$s) AS MC ON MC.%11$s = M.%4$s",
MangaTable.TABLE,
ChapterTable.TABLE,
MangaCategoryTable.TABLE,
MangaTable.COLUMN_ID,
ChapterTable.COLUMN_MANGA_ID,
MangaTable.COLUMN_UNREAD,
ChapterTable.COLUMN_READ,
MangaTable.COLUMN_FAVORITE,
MangaTable.COLUMN_TITLE,
MangaCategoryTable.COLUMN_CATEGORY_ID,
MangaCategoryTable.COLUMN_MANGA_ID,
MangaTable.COLUMN_CATEGORY
);
@Override
@NonNull
public Manga mapFromCursor(@NonNull Cursor cursor) {
Manga manga = super.mapFromCursor(cursor);
int unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD);
manga.unread = cursor.getInt(unreadColumn);
int categoryColumn = cursor.getColumnIndex(MangaTable.COLUMN_CATEGORY);
manga.category = cursor.getInt(categoryColumn);
return manga;
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor
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() {
companion object {
val INSTANCE = LibraryMangaGetResolver()
}
override fun mapFromCursor(cursor: Cursor): Manga {
val manga = super.mapFromCursor(cursor)
val unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD)
manga.unread = cursor.getInt(unreadColumn)
val categoryColumn = cursor.getColumnIndex(MangaTable.COLUMN_CATEGORY)
manga.category = cursor.getInt(categoryColumn)
return manga
}
}

View File

@ -1,56 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers;
import android.database.Cursor;
import android.support.annotation.NonNull;
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver;
import java.util.Date;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.ChapterStorIOSQLiteGetResolver;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
public class MangaChapterGetResolver extends DefaultGetResolver<MangaChapter> {
public static final MangaChapterGetResolver INSTANCE = new MangaChapterGetResolver();
public static final String QUERY = String.format(
"SELECT * FROM %1$s JOIN %2$s on %1$s.%3$s = %2$s.%4$s",
MangaTable.TABLE,
ChapterTable.TABLE,
MangaTable.COLUMN_ID,
ChapterTable.COLUMN_MANGA_ID);
public static String getRecentChaptersQuery(Date date) {
return QUERY + String.format(" WHERE %1$s = 1 AND %2$s > %3$d ORDER BY %2$s DESC",
MangaTable.COLUMN_FAVORITE,
ChapterTable.COLUMN_DATE_UPLOAD,
date.getTime());
}
@NonNull
private final MangaStorIOSQLiteGetResolver mangaGetResolver;
@NonNull
private final ChapterStorIOSQLiteGetResolver chapterGetResolver;
public MangaChapterGetResolver() {
this.mangaGetResolver = new MangaStorIOSQLiteGetResolver();
this.chapterGetResolver = new ChapterStorIOSQLiteGetResolver();
}
@NonNull
@Override
public MangaChapter mapFromCursor(@NonNull Cursor cursor) {
final Manga manga = mangaGetResolver.mapFromCursor(cursor);
final Chapter chapter = chapterGetResolver.mapFromCursor(cursor);
manga.id = chapter.manga_id;
return new MangaChapter(manga, chapter);
}
}

View File

@ -0,0 +1,28 @@
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.models.MangaChapter
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
companion object {
val INSTANCE = MangaChapterGetResolver()
}
private val mangaGetResolver = MangaStorIOSQLiteGetResolver()
private val chapterGetResolver = ChapterStorIOSQLiteGetResolver()
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"));
return MangaChapter(manga, chapter)
}
}

View File

@ -1,441 +0,0 @@
package eu.kanade.tachiyomi.data.download;
import android.content.Context;
import android.net.Uri;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
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.download.model.Download;
import eu.kanade.tachiyomi.data.download.model.DownloadQueue;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.util.DiskUtils;
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator;
import eu.kanade.tachiyomi.util.UrlUtil;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subjects.BehaviorSubject;
import rx.subjects.PublishSubject;
import timber.log.Timber;
public class DownloadManager {
private Context context;
private SourceManager sourceManager;
private PreferencesHelper preferences;
private Gson gson;
private PublishSubject<List<Download>> downloadsQueueSubject;
private BehaviorSubject<Boolean> runningSubject;
private Subscription downloadsSubscription;
private BehaviorSubject<Integer> threadsSubject;
private Subscription threadsSubscription;
private DownloadQueue queue;
private volatile boolean isRunning;
public static final String PAGE_LIST_FILE = "index.json";
public DownloadManager(Context context, SourceManager sourceManager, PreferencesHelper preferences) {
this.context = context;
this.sourceManager = sourceManager;
this.preferences = preferences;
gson = new Gson();
queue = new DownloadQueue();
downloadsQueueSubject = PublishSubject.create();
runningSubject = BehaviorSubject.create();
threadsSubject = BehaviorSubject.create();
}
private void initializeSubscriptions() {
if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed())
downloadsSubscription.unsubscribe();
threadsSubscription = preferences.downloadThreads().asObservable()
.subscribe(threadsSubject::onNext);
downloadsSubscription = downloadsQueueSubject
.flatMap(Observable::from)
.lift(new DynamicConcurrentMergeOperator<>(this::downloadChapter, threadsSubject))
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.map(download -> areAllDownloadsFinished())
.subscribe(finished -> {
if (finished) {
DownloadService.stop(context);
}
}, e -> DownloadService.stop(context));
if (!isRunning) {
isRunning = true;
runningSubject.onNext(true);
}
}
public void destroySubscriptions() {
if (isRunning) {
isRunning = false;
runningSubject.onNext(false);
}
if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed()) {
downloadsSubscription.unsubscribe();
downloadsSubscription = null;
}
if (threadsSubscription != null && !threadsSubscription.isUnsubscribed()) {
threadsSubscription.unsubscribe();
}
}
// Create a download object for every chapter in the event and add them to the downloads queue
public void onDownloadChaptersEvent(DownloadChaptersEvent event) {
final Manga manga = event.getManga();
final Source source = sourceManager.get(manga.source);
// Used to avoid downloading chapters with the same name
final List<String> addedChapters = new ArrayList<>();
final List<Download> pending = new ArrayList<>();
for (Chapter chapter : event.getChapters()) {
if (addedChapters.contains(chapter.name))
continue;
addedChapters.add(chapter.name);
Download download = new Download(source, manga, chapter);
if (!prepareDownload(download)) {
queue.add(download);
pending.add(download);
}
}
if (isRunning) downloadsQueueSubject.onNext(pending);
}
// Public method to check if a chapter is downloaded
public boolean isChapterDownloaded(Source source, Manga manga, Chapter chapter) {
File directory = getAbsoluteChapterDirectory(source, manga, chapter);
if (!directory.exists())
return false;
List<Page> pages = getSavedPageList(source, manga, chapter);
return isChapterDownloaded(directory, pages);
}
// Prepare the download. Returns true if the chapter is already downloaded
private boolean prepareDownload(Download download) {
// If the chapter is already queued, don't add it again
for (Download queuedDownload : queue) {
if (download.chapter.id.equals(queuedDownload.chapter.id))
return true;
}
// Add the directory to the download object for future access
download.directory = getAbsoluteChapterDirectory(download);
// If the directory doesn't exist, the chapter isn't downloaded.
if (!download.directory.exists()) {
return false;
}
// If the page list doesn't exist, the chapter isn't downloaded
List<Page> savedPages = getSavedPageList(download);
if (savedPages == null)
return false;
// Add the page list to the download object for future access
download.pages = savedPages;
// If the number of files matches the number of pages, the chapter is downloaded.
// We have the index file, so we check one file more
return isChapterDownloaded(download.directory, download.pages);
}
// Check that all the images are downloaded
private boolean isChapterDownloaded(File directory, List<Page> pages) {
return pages != null && !pages.isEmpty() && pages.size() + 1 == directory.listFiles().length;
}
// Download the entire chapter
private Observable<Download> downloadChapter(Download download) {
try {
DiskUtils.createDirectory(download.directory);
} catch (IOException e) {
return Observable.error(e);
}
Observable<List<Page>> pageListObservable = download.pages == null ?
// Pull page list from network and add them to download object
download.source
.pullPageListFromNetwork(download.chapter.url)
.doOnNext(pages -> download.pages = pages)
.doOnNext(pages -> savePageList(download)) :
// Or if the page list already exists, start from the file
Observable.just(download.pages);
return Observable.defer(() -> pageListObservable
.doOnNext(pages -> {
download.downloadedImages = 0;
download.setStatus(Download.DOWNLOADING);
})
// Get all the URLs to the source images, fetch pages if necessary
.flatMap(download.source::getAllImageUrlsFromPageList)
// Start downloading images, consider we can have downloaded images already
.concatMap(page -> getOrDownloadImage(page, download))
// Do after download completes
.doOnCompleted(() -> onDownloadCompleted(download))
.toList()
.map(pages -> download)
// If the page list threw, it will resume here
.onErrorResumeNext(error -> {
download.setStatus(Download.ERROR);
return Observable.just(download);
}))
.subscribeOn(Schedulers.io());
}
// Get the image from the filesystem if it exists or download from network
private Observable<Page> getOrDownloadImage(final Page page, Download download) {
// If the image URL is empty, do nothing
if (page.getImageUrl() == null)
return Observable.just(page);
String filename = getImageFilename(page);
File imagePath = new File(download.directory, filename);
// If the image is already downloaded, do nothing. Otherwise download from network
Observable<Page> pageObservable = isImageDownloaded(imagePath) ?
Observable.just(page) :
downloadImage(page, download.source, download.directory, filename);
return pageObservable
// When the image is ready, set image path, progress (just in case) and status
.doOnNext(p -> {
page.setImagePath(imagePath.getAbsolutePath());
page.setProgress(100);
download.downloadedImages++;
page.setStatus(Page.READY);
})
// Mark this page as error and allow to download the remaining
.onErrorResumeNext(e -> {
page.setProgress(0);
page.setStatus(Page.ERROR);
return Observable.just(page);
});
}
// Save image on disk
private Observable<Page> downloadImage(Page page, Source source, File directory, String filename) {
page.setStatus(Page.DOWNLOAD_IMAGE);
return source.getImageProgressResponse(page)
.flatMap(resp -> {
try {
DiskUtils.saveBufferedSourceToDirectory(resp.body().source(), directory, filename);
} catch (Exception e) {
Timber.e(e.getCause(), e.getMessage());
return Observable.error(e);
}
return Observable.just(page);
})
.retry(2);
}
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
public Observable<Page> getDownloadedImage(final Page page, File chapterDir) {
if (page.getImageUrl() == null) {
page.setStatus(Page.ERROR);
return Observable.just(page);
}
File imagePath = new File(chapterDir, getImageFilename(page));
// When the image is ready, set image path, progress (just in case) and status
if (isImageDownloaded(imagePath)) {
page.setImagePath(imagePath.getAbsolutePath());
page.setProgress(100);
page.setStatus(Page.READY);
} else {
page.setStatus(Page.ERROR);
}
return Observable.just(page);
}
// Get the filename for an image given the page
private String getImageFilename(Page page) {
String url = page.getImageUrl();
int number = page.getPageNumber() + 1;
// Try to preserve file extension
if (UrlUtil.isJpg(url)) {
return number + ".jpg";
} else if (UrlUtil.isPng(url)) {
return number + ".png";
} else if (UrlUtil.isGif(url)) {
return number + ".gif";
}
return Uri.parse(url).getLastPathSegment().replaceAll("[^\\sa-zA-Z0-9.-]", "_");
}
private boolean isImageDownloaded(File imagePath) {
return imagePath.exists();
}
// Called when a download finishes. This doesn't mean the download was successful, so we check it
private void onDownloadCompleted(final Download download) {
checkDownloadIsSuccessful(download);
savePageList(download);
}
private void checkDownloadIsSuccessful(final Download download) {
int actualProgress = 0;
int status = Download.DOWNLOADED;
// If any page has an error, the download result will be error
for (Page page : download.pages) {
actualProgress += page.getProgress();
if (page.getStatus() != Page.READY) status = Download.ERROR;
}
// Ensure that the chapter folder has all the images
if (!isChapterDownloaded(download.directory, download.pages)) {
status = Download.ERROR;
}
download.totalProgress = actualProgress;
download.setStatus(status);
// Delete successful downloads from queue after notifying
if (status == Download.DOWNLOADED) {
queue.remove(download);
}
}
// Return the page list from the chapter's directory if it exists, null otherwise
public List<Page> getSavedPageList(Source source, Manga manga, Chapter chapter) {
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
JsonReader reader = null;
try {
if (pagesFile.exists()) {
reader = new JsonReader(new FileReader(pagesFile.getAbsolutePath()));
Type collectionType = new TypeToken<List<Page>>() {}.getType();
return gson.fromJson(reader, collectionType);
}
} catch (Exception e) {
Timber.e(e.getCause(), e.getMessage());
} finally {
if (reader != null) try { reader.close(); } catch (IOException e) { /* Do nothing */ }
}
return null;
}
// Shortcut for the method above
private List<Page> getSavedPageList(Download download) {
return getSavedPageList(download.source, download.manga, download.chapter);
}
// Save the page list to the chapter's directory
public void savePageList(Source source, Manga manga, Chapter chapter, List<Page> pages) {
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
FileOutputStream out = null;
try {
out = new FileOutputStream(pagesFile);
out.write(gson.toJson(pages).getBytes());
out.flush();
} catch (IOException e) {
Timber.e(e.getCause(), e.getMessage());
} finally {
if (out != null) try { out.close(); } catch (IOException e) { /* Do nothing */ }
}
}
// Shortcut for the method above
private void savePageList(Download download) {
savePageList(download.source, download.manga, download.chapter, download.pages);
}
// Get the absolute path to the chapter directory
public File getAbsoluteChapterDirectory(Source source, Manga manga, Chapter chapter) {
String chapterRelativePath = source.getName() +
File.separator +
manga.title.replaceAll("[^\\sa-zA-Z0-9.-]", "_") +
File.separator +
chapter.name.replaceAll("[^\\sa-zA-Z0-9.-]", "_");
return new File(preferences.getDownloadsDirectory(), chapterRelativePath);
}
// Shortcut for the method above
private File getAbsoluteChapterDirectory(Download download) {
return getAbsoluteChapterDirectory(download.source, download.manga, download.chapter);
}
public void deleteChapter(Source source, Manga manga, Chapter chapter) {
File path = getAbsoluteChapterDirectory(source, manga, chapter);
DiskUtils.deleteFiles(path);
}
public DownloadQueue getQueue() {
return queue;
}
public boolean areAllDownloadsFinished() {
for (Download download : queue) {
if (download.getStatus() <= Download.DOWNLOADING)
return false;
}
return true;
}
public boolean startDownloads() {
if (queue.isEmpty())
return false;
if (downloadsSubscription == null)
initializeSubscriptions();
final List<Download> pending = new ArrayList<>();
for (Download download : queue) {
if (download.getStatus() != Download.DOWNLOADED) {
if (download.getStatus() != Download.QUEUE) download.setStatus(Download.QUEUE);
pending.add(download);
}
}
downloadsQueueSubject.onNext(pending);
return !pending.isEmpty();
}
public void stopDownloads() {
destroySubscriptions();
for (Download download : queue) {
if (download.getStatus() == Download.DOWNLOADING) {
download.setStatus(Download.ERROR);
}
}
}
public BehaviorSubject<Boolean> getRunningSubject() {
return runningSubject;
}
}

View File

@ -0,0 +1,424 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.net.Uri
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.util.DiskUtils
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
import eu.kanade.tachiyomi.util.UrlUtil
import eu.kanade.tachiyomi.util.toast
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.FileReader
import java.io.IOException
import java.util.*
class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) {
private val gson = Gson()
private val downloadsQueueSubject = PublishSubject.create<List<Download>>()
val runningSubject = BehaviorSubject.create<Boolean>()
private var downloadsSubscription: Subscription? = null
private val threadsSubject = BehaviorSubject.create<Int>()
private var threadsSubscription: Subscription? = null
val queue = DownloadQueue()
val imageFilenameRegex = "[^\\sa-zA-Z0-9.-]".toRegex()
val PAGE_LIST_FILE = "index.json"
@Volatile private var isRunning: Boolean = false
private fun initializeSubscriptions() {
downloadsSubscription?.unsubscribe()
threadsSubscription = preferences.downloadThreads().asObservable()
.subscribe { threadsSubject.onNext(it) }
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.map { download -> areAllDownloadsFinished() }
.subscribe({ finished ->
if (finished!!) {
DownloadService.stop(context)
}
}, { e ->
DownloadService.stop(context)
Timber.e(e, e.message)
context.toast(e.message)
})
if (!isRunning) {
isRunning = true
runningSubject.onNext(true)
}
}
fun destroySubscriptions() {
if (isRunning) {
isRunning = false
runningSubject.onNext(false)
}
if (downloadsSubscription != null) {
downloadsSubscription?.unsubscribe()
downloadsSubscription = null
}
if (threadsSubscription != null) {
threadsSubscription?.unsubscribe()
}
}
// Create a download object for every chapter in the event and add them to the downloads queue
fun onDownloadChaptersEvent(event: DownloadChaptersEvent) {
val manga = event.manga
val source = sourceManager.get(manga.source)
// Used to avoid downloading chapters with the same name
val addedChapters = ArrayList<String>()
val pending = ArrayList<Download>()
for (chapter in event.chapters) {
if (addedChapters.contains(chapter.name))
continue
addedChapters.add(chapter.name)
val download = Download(source, manga, chapter)
if (!prepareDownload(download)) {
queue.add(download)
pending.add(download)
}
}
if (isRunning) downloadsQueueSubject.onNext(pending)
}
// Public method to check if a chapter is downloaded
fun isChapterDownloaded(source: Source, manga: Manga, chapter: Chapter): Boolean {
val directory = getAbsoluteChapterDirectory(source, manga, chapter)
if (!directory.exists())
return false
val pages = getSavedPageList(source, manga, chapter)
return isChapterDownloaded(directory, pages)
}
// Prepare the download. Returns true if the chapter is already downloaded
private fun prepareDownload(download: Download): Boolean {
// If the chapter is already queued, don't add it again
for (queuedDownload in queue) {
if (download.chapter.id == queuedDownload.chapter.id)
return true
}
// Add the directory to the download object for future access
download.directory = getAbsoluteChapterDirectory(download)
// If the directory doesn't exist, the chapter isn't downloaded.
if (!download.directory.exists()) {
return false
}
// If the page list doesn't exist, the chapter isn't downloaded
val savedPages = getSavedPageList(download) ?: return false
// Add the page list to the download object for future access
download.pages = savedPages
// If the number of files matches the number of pages, the chapter is downloaded.
// We have the index file, so we check one file more
return isChapterDownloaded(download.directory, download.pages)
}
// Check that all the images are downloaded
private fun isChapterDownloaded(directory: File, pages: List<Page>?): Boolean {
return pages != null && !pages.isEmpty() && pages.size + 1 == directory.listFiles().size
}
// Download the entire chapter
private fun downloadChapter(download: Download): Observable<Download> {
DiskUtils.createDirectory(download.directory)
val pageListObservable = if (download.pages == null)
// Pull page list from network and add them to download object
download.source.pullPageListFromNetwork(download.chapter.url)
.doOnNext { pages ->
download.pages = pages
savePageList(download)
}
else
// Or if the page list already exists, start from the file
Observable.just(download.pages)
return Observable.defer<Download> { pageListObservable
.doOnNext { pages ->
download.downloadedImages = 0
download.status = Download.DOWNLOADING
}
// Get all the URLs to the source images, fetch pages if necessary
.flatMap { download.source.getAllImageUrlsFromPageList(it) }
// Start downloading images, consider we can have downloaded images already
.concatMap { page -> getOrDownloadImage(page, download) }
// Do after download completes
.doOnCompleted { onDownloadCompleted(download) }
.toList()
.map { pages -> download }
// If the page list threw, it will resume here
.onErrorResumeNext { error ->
download.status = Download.ERROR
Observable.just(download)
}
}.subscribeOn(Schedulers.io())
}
// Get the image from the filesystem if it exists or download from network
private fun getOrDownloadImage(page: Page, download: Download): Observable<Page> {
// If the image URL is empty, do nothing
if (page.imageUrl == null)
return Observable.just(page)
val filename = getImageFilename(page)
val imagePath = File(download.directory, filename)
// If the image is already downloaded, do nothing. Otherwise download from network
val pageObservable = if (isImageDownloaded(imagePath))
Observable.just(page)
else
downloadImage(page, download.source, download.directory, filename)
return pageObservable
// When the image is ready, set image path, progress (just in case) and status
.doOnNext {
page.imagePath = imagePath.absolutePath
page.progress = 100
download.downloadedImages++
page.status = Page.READY
}
// Mark this page as error and allow to download the remaining
.onErrorResumeNext {
page.progress = 0
page.status = Page.ERROR
Observable.just(page)
}
}
// Save image on disk
private fun downloadImage(page: Page, source: Source, directory: File, filename: String): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE
return source.getImageProgressResponse(page)
.flatMap({ resp ->
DiskUtils.saveBufferedSourceToDirectory(resp.body().source(), directory, filename)
Observable.just(page)
}).retry(2)
}
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
fun getDownloadedImage(page: Page, chapterDir: File): Observable<Page> {
if (page.imageUrl == null) {
page.status = Page.ERROR
return Observable.just(page)
}
val imagePath = File(chapterDir, getImageFilename(page))
// When the image is ready, set image path, progress (just in case) and status
if (isImageDownloaded(imagePath)) {
page.imagePath = imagePath.absolutePath
page.progress = 100
page.status = Page.READY
} else {
page.status = Page.ERROR
}
return Observable.just(page)
}
// Get the filename for an image given the page
private fun getImageFilename(page: Page): String {
val url = page.imageUrl
val number = String.format("%03d", page.pageNumber + 1)
// Try to preserve file extension
return when {
UrlUtil.isJpg(url) -> "$number.jpg"
UrlUtil.isPng(url) -> "$number.png"
UrlUtil.isGif(url) -> "$number.gif"
else -> Uri.parse(url).lastPathSegment.replace(imageFilenameRegex, "_")
}
}
private fun isImageDownloaded(imagePath: File): Boolean {
return imagePath.exists()
}
// Called when a download finishes. This doesn't mean the download was successful, so we check it
private fun onDownloadCompleted(download: Download) {
checkDownloadIsSuccessful(download)
savePageList(download)
}
private fun checkDownloadIsSuccessful(download: Download) {
var actualProgress = 0
var status = Download.DOWNLOADED
// If any page has an error, the download result will be error
for (page in download.pages) {
actualProgress += page.progress
if (page.status != Page.READY) status = Download.ERROR
}
// Ensure that the chapter folder has all the images
if (!isChapterDownloaded(download.directory, download.pages)) {
status = Download.ERROR
}
download.totalProgress = actualProgress
download.status = status
// Delete successful downloads from queue after notifying
if (status == Download.DOWNLOADED) {
queue.del(download)
}
}
// Return the page list from the chapter's directory if it exists, null otherwise
fun getSavedPageList(source: Source, manga: Manga, chapter: Chapter): List<Page>? {
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
var reader: JsonReader? = null
try {
if (pagesFile.exists()) {
reader = JsonReader(FileReader(pagesFile.absolutePath))
val collectionType = object : TypeToken<List<Page>>() {
}.type
return gson.fromJson<List<Page>>(reader, collectionType)
}
} catch (e: Exception) {
Timber.e(e.cause, e.message)
} finally {
if (reader != null) try {
reader.close()
} catch (e: IOException) {
/* Do nothing */
}
}
return null
}
// Shortcut for the method above
private fun getSavedPageList(download: Download): List<Page>? {
return getSavedPageList(download.source, download.manga, download.chapter)
}
// Save the page list to the chapter's directory
fun savePageList(source: Source, manga: Manga, chapter: Chapter, pages: List<Page>) {
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
var out: FileOutputStream? = null
try {
out = FileOutputStream(pagesFile)
out.write(gson.toJson(pages).toByteArray())
out.flush()
} catch (e: IOException) {
Timber.e(e.cause, e.message)
} finally {
if (out != null) try {
out.close()
} catch (e: IOException) {
/* Do nothing */
}
}
}
// Shortcut for the method above
private fun savePageList(download: Download) {
savePageList(download.source, download.manga, download.chapter, download.pages)
}
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
val mangaRelativePath = source.visibleName +
File.separator +
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
return File(preferences.downloadsDirectory, mangaRelativePath)
}
// Get the absolute path to the chapter directory
fun getAbsoluteChapterDirectory(source: Source, manga: Manga, chapter: Chapter): File {
val chapterRelativePath = chapter.name.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
return File(getAbsoluteMangaDirectory(source, manga), chapterRelativePath)
}
// Shortcut for the method above
private fun getAbsoluteChapterDirectory(download: Download): File {
return getAbsoluteChapterDirectory(download.source, download.manga, download.chapter)
}
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
val path = getAbsoluteChapterDirectory(source, manga, chapter)
DiskUtils.deleteFiles(path)
}
fun areAllDownloadsFinished(): Boolean {
for (download in queue) {
if (download.status <= Download.DOWNLOADING)
return false
}
return true
}
fun startDownloads(): Boolean {
if (queue.isEmpty())
return false
if (downloadsSubscription == null || downloadsSubscription!!.isUnsubscribed)
initializeSubscriptions()
val pending = ArrayList<Download>()
for (download in queue) {
if (download.status != Download.DOWNLOADED) {
if (download.status != Download.QUEUE) download.status = Download.QUEUE
pending.add(download)
}
}
downloadsQueueSubject.onNext(pending)
return !pending.isEmpty()
}
fun stopDownloads() {
destroySubscriptions()
for (download in queue) {
if (download.status == Download.DOWNLOADING) {
download.status = Download.ERROR
}
}
}
}

View File

@ -1,151 +0,0 @@
package eu.kanade.tachiyomi.data.download;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.PowerManager;
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import javax.inject.Inject;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class DownloadService extends Service {
@Inject DownloadManager downloadManager;
@Inject PreferencesHelper preferences;
private PowerManager.WakeLock wakeLock;
private Subscription networkChangeSubscription;
private Subscription queueRunningSubscription;
private boolean isRunning;
public static void start(Context context) {
context.startService(new Intent(context, DownloadService.class));
}
public static void stop(Context context) {
context.stopService(new Intent(context, DownloadService.class));
}
@Override
public void onCreate() {
super.onCreate();
App.get(this).getComponent().inject(this);
createWakeLock();
listenQueueRunningChanges();
EventBus.getDefault().register(this);
listenNetworkChanges();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public void onDestroy() {
EventBus.getDefault().unregister(this);
queueRunningSubscription.unsubscribe();
networkChangeSubscription.unsubscribe();
downloadManager.destroySubscriptions();
destroyWakeLock();
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEvent(DownloadChaptersEvent event) {
EventBus.getDefault().removeStickyEvent(event);
downloadManager.onDownloadChaptersEvent(event);
}
private void listenNetworkChanges() {
networkChangeSubscription = new ReactiveNetwork().enableInternetCheck()
.observeConnectivity(getApplicationContext())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> {
switch (state) {
case WIFI_CONNECTED_HAS_INTERNET:
// If there are no remaining downloads, destroy the service
if (!isRunning && !downloadManager.startDownloads()) {
stopSelf();
}
break;
case MOBILE_CONNECTED:
if (!preferences.downloadOnlyOverWifi()) {
if (!isRunning && !downloadManager.startDownloads()) {
stopSelf();
}
} else if (isRunning) {
downloadManager.stopDownloads();
}
break;
default:
if (isRunning) {
downloadManager.stopDownloads();
}
break;
}
}, error -> {
ToastUtil.showShort(this, R.string.download_queue_error);
stopSelf();
});
}
private void listenQueueRunningChanges() {
queueRunningSubscription = downloadManager.getRunningSubject()
.subscribe(running -> {
isRunning = running;
if (running)
acquireWakeLock();
else
releaseWakeLock();
});
}
private void createWakeLock() {
wakeLock = ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock");
}
private void destroyWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
}
public void acquireWakeLock() {
if (wakeLock != null && !wakeLock.isHeld()) {
wakeLock.acquire();
}
}
public void releaseWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}

View File

@ -0,0 +1,134 @@
package eu.kanade.tachiyomi.data.download
import android.app.Service
import android.content.Context
import android.content.Intent
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
class DownloadService : Service() {
companion object {
fun start(context: Context) {
context.startService(Intent(context, DownloadService::class.java))
}
fun stop(context: Context) {
context.stopService(Intent(context, DownloadService::class.java))
}
}
@Inject lateinit var downloadManager: DownloadManager
@Inject lateinit var preferences: PreferencesHelper
private var wakeLock: PowerManager.WakeLock? = null
private var networkChangeSubscription: Subscription? = null
private var queueRunningSubscription: Subscription? = null
private var isRunning: Boolean = false
override fun onCreate() {
super.onCreate()
App.get(this).component.inject(this)
createWakeLock()
listenQueueRunningChanges()
listenNetworkChanges()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return Service.START_STICKY
}
override fun onDestroy() {
queueRunningSubscription?.unsubscribe()
networkChangeSubscription?.unsubscribe()
downloadManager.destroySubscriptions()
destroyWakeLock()
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder? {
return null
}
private fun listenNetworkChanges() {
networkChangeSubscription = ReactiveNetwork().enableInternetCheck()
.observeConnectivity(applicationContext)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ state ->
when (state) {
ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET -> {
// If there are no remaining downloads, destroy the service
if (!isRunning && !downloadManager.startDownloads()) {
stopSelf()
}
}
ConnectivityStatus.MOBILE_CONNECTED -> {
if (!preferences.downloadOnlyOverWifi()) {
if (!isRunning && !downloadManager.startDownloads()) {
stopSelf()
}
} else if (isRunning) {
downloadManager.stopDownloads()
}
}
else -> {
if (isRunning) {
downloadManager.stopDownloads()
}
}
}
}, { error ->
toast(R.string.download_queue_error)
stopSelf()
})
}
private fun listenQueueRunningChanges() {
queueRunningSubscription = downloadManager.runningSubject.subscribe { running ->
isRunning = running
if (running)
acquireWakeLock()
else
releaseWakeLock()
}
}
private fun createWakeLock() {
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock")
}
private fun destroyWakeLock() {
if (wakeLock != null && wakeLock!!.isHeld) {
wakeLock!!.release()
wakeLock = null
}
}
fun acquireWakeLock() {
if (wakeLock != null && !wakeLock!!.isHeld) {
wakeLock!!.acquire()
}
}
fun releaseWakeLock() {
if (wakeLock != null && wakeLock!!.isHeld) {
wakeLock!!.release()
}
}
}

View File

@ -1,78 +0,0 @@
package eu.kanade.tachiyomi.data.download.model;
import java.util.ArrayList;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.source.model.Page;
import rx.Observable;
import rx.subjects.PublishSubject;
public class DownloadQueue extends ArrayList<Download> {
private PublishSubject<Download> statusSubject;
public DownloadQueue() {
super();
statusSubject = PublishSubject.create();
}
public boolean add(Download download) {
download.setStatusSubject(statusSubject);
download.setStatus(Download.QUEUE);
return super.add(download);
}
public void remove(Download download) {
super.remove(download);
download.setStatusSubject(null);
}
public void remove(Chapter chapter) {
for (Download download : this) {
if (download.chapter.id.equals(chapter.id)) {
remove(download);
break;
}
}
}
public Observable<Download> getActiveDownloads() {
return Observable.from(this)
.filter(download -> download.getStatus() == Download.DOWNLOADING);
}
public Observable<Download> getStatusObservable() {
return statusSubject.onBackpressureBuffer();
}
public Observable<Download> getProgressObservable() {
return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads())
.flatMap(download -> {
if (download.getStatus() == Download.DOWNLOADING) {
PublishSubject<Integer> pageStatusSubject = PublishSubject.create();
setPagesSubject(download.pages, pageStatusSubject);
return pageStatusSubject
.filter(status -> status == Page.READY)
.map(status -> download);
} else if (download.getStatus() == Download.DOWNLOADED ||
download.getStatus() == Download.ERROR) {
setPagesSubject(download.pages, null);
}
return Observable.just(download);
})
.filter(download -> download.getStatus() == Download.DOWNLOADING);
}
private void setPagesSubject(List<Page> pages, PublishSubject<Integer> subject) {
if (pages != null) {
for (Page page : pages) {
page.setStatusSubject(subject);
}
}
}
}

View File

@ -0,0 +1,65 @@
package eu.kanade.tachiyomi.data.download.model
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.source.model.Page
import rx.Observable
import rx.subjects.PublishSubject
import java.util.*
class DownloadQueue : ArrayList<Download>() {
private val statusSubject = PublishSubject.create<Download>()
override fun add(download: Download): Boolean {
download.setStatusSubject(statusSubject)
download.status = Download.QUEUE
return super.add(download)
}
fun del(download: Download) {
super.remove(download)
download.setStatusSubject(null)
}
fun del(chapter: Chapter) {
for (download in this) {
if (download.chapter.id == chapter.id) {
del(download)
break
}
}
}
fun getActiveDownloads() =
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
fun getStatusObservable() = statusSubject.onBackpressureBuffer()
fun getProgressObservable(): Observable<Download> {
return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads())
.flatMap { download ->
if (download.status == Download.DOWNLOADING) {
val pageStatusSubject = PublishSubject.create<Int>()
setPagesSubject(download.pages, pageStatusSubject)
return@flatMap pageStatusSubject
.filter { it == Page.READY }
.map { download }
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null)
}
Observable.just(download)
}
.filter { it.status == Download.DOWNLOADING }
}
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
if (pages != null) {
for (page in pages) {
page.setStatusSubject(subject)
}
}
}
}

View File

@ -1,110 +0,0 @@
package eu.kanade.tachiyomi.data.io;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class IOHandler {
/**
* Get full filepath of build in Android File picker.
* If Google Drive (or other Cloud service) throw exception and download before loading
*/
public static String getFilePath(Uri uri, ContentResolver resolver, Context context) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
String filePath = "";
String wholeID = DocumentsContract.getDocumentId(uri);
//Ugly work around. In sdk version Kitkat or higher external getDocumentId request will have no content://
if (wholeID.split(":").length == 1)
throw new IllegalArgumentException();
// Split at colon, use second item in the array
String id = wholeID.split(":")[1];
String[] column = {MediaStore.Images.Media.DATA};
// where id is equal to
String sel = MediaStore.Images.Media._ID + "=?";
Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
column, sel, new String[]{id}, null);
int columnIndex = cursor != null ? cursor.getColumnIndex(column[0]) : 0;
if (cursor != null ? cursor.moveToFirst() : false) {
filePath = cursor.getString(columnIndex);
}
cursor.close();
return filePath;
} else {
String[] fields = {MediaStore.Images.Media.DATA};
Cursor cursor = resolver.query(uri, fields, null, null, null);
if (cursor == null)
return null;
cursor.moveToFirst();
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
cursor.close();
return path;
}
} catch (IllegalArgumentException e) {
//This exception is thrown when Google Drive. Try to download file
return downloadMediaAndReturnPath(uri, resolver, context);
}
}
private static String getTempFilename(Context context) throws IOException {
File outputDir = context.getCacheDir();
File outputFile = File.createTempFile("temp_cover", "0", outputDir);
return outputFile.getAbsolutePath();
}
private static String downloadMediaAndReturnPath(Uri uri, ContentResolver resolver, Context context) {
if (uri == null) return null;
FileInputStream input = null;
FileOutputStream output = null;
try {
ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
FileDescriptor fd = pfd != null ? pfd.getFileDescriptor() : null;
input = new FileInputStream(fd);
String tempFilename = getTempFilename(context);
output = new FileOutputStream(tempFilename);
int read;
byte[] bytes = new byte[4096];
while ((read = input.read(bytes)) != -1) {
output.write(bytes, 0, read);
}
return tempFilename;
} catch (IOException ignored) {
} finally {
if (input != null) try {
input.close();
} catch (Exception ignored) {
}
if (output != null) try {
output.close();
} catch (Exception ignored) {
}
}
return null;
}
}

View File

@ -0,0 +1,82 @@
package eu.kanade.tachiyomi.data.library
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.alarmManager
/**
* This class is used to update the library by firing an alarm after a specified time.
* It has a receiver reacting to system's boot and the intent fired by this alarm.
* See [onReceive] for more information.
*/
class LibraryUpdateAlarm : BroadcastReceiver() {
companion object {
const val LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY"
/**
* Sets the alarm to run the intent that updates the library.
* @param context the application context.
* @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)) {
// Stop previous running alarms if needed, and do not restart it if the interval is 0.
stopAlarm(context)
if (intervalInHours == 0)
return
// Get the time the alarm should fire the event to update.
val intervalInMillis = intervalInHours * 60 * 60 * 1000
val nextRun = SystemClock.elapsedRealtime() + intervalInMillis
// Start the alarm.
val pendingIntent = getPendingIntent(context)
context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
nextRun, intervalInMillis.toLong(), pendingIntent)
}
/**
* Stops the alarm if it's running.
* @param context the application context.
*/
fun stopAlarm(context: Context) {
val pendingIntent = getPendingIntent(context)
context.alarmManager.cancel(pendingIntent)
}
/**
* Get the intent the alarm should run when it's fired.
* @param context the application context.
* @return the intent that will run when the alarm is fired.
*/
private fun getPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, LibraryUpdateAlarm::class.java)
intent.action = LIBRARY_UPDATE_ACTION
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
/**
* Handle the intents received by this [BroadcastReceiver].
* @param context the application context.
* @param intent the intent to process.
*/
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// Start the alarm when the system is booted.
Intent.ACTION_BOOT_COMPLETED -> startAlarm(context)
// Update the library when the alarm fires an event.
LIBRARY_UPDATE_ACTION -> LibraryUpdateService.start(context)
}
}
}

View File

@ -0,0 +1,429 @@
package eu.kanade.tachiyomi.data.library
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import android.util.Pair
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.AndroidComponentUtil
import eu.kanade.tachiyomi.util.DeviceUtil
import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
// Intent key for forced library update
val UPDATE_IS_FORCED = "is_forced"
/**
* Get the start intent for [LibraryUpdateService].
* @param context the application context.
* @param isForced true when forcing library update
* @return the intent of the service.
*/
fun getIntent(context: Context, isForced: Boolean = false): Intent {
return Intent(context, LibraryUpdateService::class.java).apply {
putExtra(UPDATE_IS_FORCED, isForced)
}
}
/**
* Returns the status of the service.
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
}
/**
* This class will take care of updating the chapters of the manga from the library. It can be
* started calling the [start] method. If it's already running, it won't do anything.
* While the library is updating, a [PowerManager.WakeLock] will be held until the update is
* completed, preventing the device from going to sleep mode. A notification will display the
* progress of the update, and if case of an unexpected error, this service will be silently
* destroyed.
*/
class LibraryUpdateService : Service() {
// Dependencies injected through dagger.
@Inject lateinit var db: DatabaseHelper
@Inject lateinit var sourceManager: SourceManager
@Inject lateinit var preferences: PreferencesHelper
// Wake lock that will be held until the service is destroyed.
private lateinit var wakeLock: PowerManager.WakeLock
// Subscription where the update is done.
private var subscription: Subscription? = null
companion object {
val UPDATE_NOTIFICATION_ID = 1
/**
* Static method to start the service. It will be started only if there isn't another
* instance already running.
* @param context the application context.
*/
@JvmStatic
fun start(context: Context, isForced: Boolean = false) {
if (!isRunning(context)) {
context.startService(getIntent(context, isForced))
}
}
fun stop(context: Context) {
context.stopService(getIntent(context))
}
}
/**
* Method called when the service is created. It injects dagger dependencies and acquire
* the wake lock.
*/
override fun onCreate() {
super.onCreate()
App.get(this).component.inject(this)
createAndAcquireWakeLock()
}
/**
* Method called when the service is destroyed. It destroy the running subscription, resets
* the alarm and release the wake lock.
*/
override fun onDestroy() {
subscription?.unsubscribe()
LibraryUpdateAlarm.startAlarm(this)
destroyWakeLock()
super.onDestroy()
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? {
return null
}
/**
* Method called when the service receives an intent. In this case, the content of the intent
* is irrelevant, because everything required is fetched in [updateLibrary].
* @param intent the intent from [start].
* @param flags the flags of the command.
* @param startId the start id of this command.
* @return the start value of the command.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// If there's no network available, set a component to start this service again when
// a connection is available.
if (!DeviceUtil.isNetworkConnected(this)) {
Timber.i("Sync canceled, connection not available")
showWarningNotification(getString(R.string.notification_no_connection_title),
getString(R.string.notification_no_connection_body))
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true)
stopSelf(startId)
return Service.START_NOT_STICKY
}
// If user doesn't want to update while phone is not charging, cancel sync
else if (preferences.updateOnlyWhenCharging() && !(intent?.getBooleanExtra(UPDATE_IS_FORCED, false) ?: false) && !DeviceUtil.isPowerConnected(this)) {
Timber.i("Sync canceled, not connected to ac power")
// Create force library update intent
val forceIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.FORCE_LIBRARY_UPDATE)
// Show warning
showWarningNotification(getString(R.string.notification_not_connected_to_ac_title),
getString(R.string.notification_not_connected_to_ac_body), forceIntent)
stopSelf(startId)
return Service.START_NOT_STICKY
}
// Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe()
// Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable.defer { updateLibrary() }
.subscribeOn(Schedulers.io())
.subscribe({},
{
showNotification(getString(R.string.notification_update_error), "")
stopSelf(startId)
}, {
stopSelf(startId)
})
return Service.START_STICKY
}
/**
* Creates a PendingIntent for LibraryUpdate broadcast class
* @param action id of action
*/
fun getLibraryUpdateReceiverIntent(action: String): PendingIntent {
return PendingIntent.getBroadcast(this, 0,
Intent(this, LibraryUpdateReceiver::class.java).apply { this.action = action }, 0)
}
/**
* Method that updates the library. It's called in a background thread, so it's safe to do
* heavy operations or network calls here.
* For each manga it calls [updateManga] and updates the notification showing the current
* progress.
* @return an observable delivering the progress of each update.
*/
fun updateLibrary(): Observable<Manga> {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
val newUpdates = ArrayList<Manga>()
val failedUpdates = ArrayList<Manga>()
val cancelIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.CANCEL_LIBRARY_UPDATE)
// Get the manga list that is going to be updated.
val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking()
val toUpdate = if (!preferences.updateOnlyNonCompleted())
allLibraryMangas
else
allLibraryMangas.filter { it.status != Manga.COMPLETED }
// Emit each manga and update it sequentially.
return Observable.from(toUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, toUpdate.size, cancelIntent) }
// Update the chapters of the manga.
.concatMap { manga ->
updateManga(manga)
// If there's any error, return empty update and continue.
.onErrorReturn {
failedUpdates.add(manga)
Pair(0, 0)
}
// Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first > 0 }
// Convert to the manga that contains new chapters.
.map { manga }
}
// Add manga with new chapters to the list.
.doOnNext { newUpdates.add(it) }
// Notify result of the overall update.
.doOnCompleted {
if (newUpdates.isEmpty()) {
cancelNotification()
} else {
showResultNotification(newUpdates, failedUpdates)
}
}
}
/**
* Updates the chapters for the given manga and adds them to the database.
* @param manga the manga to update.
* @return a pair of the inserted and removed chapters.
*/
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
val source = sourceManager.get(manga.source)
return source!!
.pullChaptersFromNetwork(manga.url)
.flatMap { db.insertOrRemoveChapters(manga, it, source) }
}
/**
* Returns the text that will be displayed in the notification when there are new chapters.
* @param updates a list of manga that contains new chapters.
* @param failedUpdates a list of manga that failed to update.
* @return the body of the notification to display.
*/
private fun getUpdatedMangasBody(updates: List<Manga>, failedUpdates: List<Manga>): String {
return with(StringBuilder()) {
if (updates.isEmpty()) {
append(getString(R.string.notification_no_new_chapters))
append("\n")
} else {
append(getString(R.string.notification_new_chapters))
for (manga in updates) {
append("\n")
append(manga.title)
}
}
if (!failedUpdates.isEmpty()) {
append("\n\n")
append(getString(R.string.notification_manga_update_failed))
for (manga in failedUpdates) {
append("\n")
append(manga.title)
}
}
toString()
}
}
/**
* Creates and acquires a wake lock until the library is updated.
*/
private fun createAndAcquireWakeLock() {
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
wakeLock.acquire()
}
/**
* Releases the wake lock if it's held.
*/
private fun destroyWakeLock() {
if (wakeLock.isHeld) {
wakeLock.release()
}
}
/**
* Shows the notification with the given title and body.
* @param title the title of the notification.
* @param body the body of the notification.
*/
private fun showNotification(title: String, body: String) {
val n = notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(title)
setContentText(body)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
}
/**
* Shows the notification containing the currently updating manga and the progress.
* @param manga the manga that's being updated.
* @param current the current progress.
* @param total the total progress.
*/
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
val n = notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(manga.title)
setProgress(total, current, false)
setOngoing(true)
addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
}
/**
* Show warning message when library can't be updated
* @param warningTitle title of warning
* @param warningBody warning information
* @param pendingIntent Intent called when action clicked
*/
private fun showWarningNotification(warningTitle: String, warningBody: String, pendingIntent: PendingIntent? = null) {
val n = notification() {
setSmallIcon(R.drawable.ic_warning_white_24dp_img)
setContentTitle(warningTitle)
setStyle(NotificationCompat.BigTextStyle().bigText(warningBody))
setContentIntent(notificationIntent)
if (pendingIntent != null) {
addAction(R.drawable.ic_refresh_grey_24dp_img, getString(R.string.action_force), pendingIntent)
}
setAutoCancel(true)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
}
/**
* Shows the notification containing the result of the update done by the service.
* @param updates a list of manga with new updates.
* @param failed a list of manga that failed to update.
*/
private fun showResultNotification(updates: List<Manga>, failed: List<Manga>) {
val title = getString(R.string.notification_update_completed)
val body = getUpdatedMangasBody(updates, failed)
val n = notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(title)
setStyle(NotificationCompat.BigTextStyle().bigText(body))
setContentIntent(notificationIntent)
setAutoCancel(true)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
}
/**
* Cancels the notification.
*/
private fun cancelNotification() {
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
}
/**
* Property that returns an intent to open the main activity.
*/
private val notificationIntent: PendingIntent
get() {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/**
* Class that triggers the library to update when a connection is available. It receives
* network changes.
*/
class SyncOnConnectionAvailable : BroadcastReceiver() {
/**
* Method called when a network change occurs.
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
if (DeviceUtil.isNetworkConnected(context)) {
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
context.startService(getIntent(context))
}
}
}
/**
* Class that triggers the library to update.
*/
class LibraryUpdateReceiver : BroadcastReceiver() {
companion object {
// Cancel library update action
val CANCEL_LIBRARY_UPDATE = "eu.kanade.CANCEL_LIBRARY_UPDATE"
// Force library update
val FORCE_LIBRARY_UPDATE = "eu.kanade.FORCE_LIBRARY_UPDATE"
}
/**
* Method called when user wants a library update.
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
CANCEL_LIBRARY_UPDATE -> {
LibraryUpdateService.stop(context)
context.notificationManager.cancel(UPDATE_NOTIFICATION_ID)
}
FORCE_LIBRARY_UPDATE -> LibraryUpdateService.start(context, true)
}
}
}
}

View File

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
public class MangaSyncManager {
private List<MangaSyncService> services;
private MyAnimeList myAnimeList;
public static final int MYANIMELIST = 1;
public MangaSyncManager(Context context) {
services = new ArrayList<>();
myAnimeList = new MyAnimeList(context);
services.add(myAnimeList);
}
public MyAnimeList getMyAnimeList() {
return myAnimeList;
}
public List<MangaSyncService> getSyncServices() {
return services;
}
public MangaSyncService getSyncService(int id) {
switch (id) {
case MYANIMELIST:
return myAnimeList;
}
return null;
}
}

View File

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.data.mangasync
import android.content.Context
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList
class MangaSyncManager(private val context: Context) {
val services: List<MangaSyncService>
val myAnimeList: MyAnimeList
companion object {
const val MYANIMELIST = 1
}
init {
myAnimeList = MyAnimeList(context, MYANIMELIST)
services = listOf(myAnimeList)
}
fun getService(id: Int): MangaSyncService = services.find { it.id == id }!!
}

View File

@ -0,0 +1,78 @@
package eu.kanade.tachiyomi.data.mangasync
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
class UpdateMangaSyncService : Service() {
@Inject lateinit var syncManager: MangaSyncManager
@Inject lateinit var db: DatabaseHelper
private lateinit var subscriptions: CompositeSubscription
override fun onCreate() {
super.onCreate()
App.get(this).component.inject(this)
subscriptions = CompositeSubscription()
}
override fun onDestroy() {
subscriptions.unsubscribe()
super.onDestroy()
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val manga = intent.getSerializableExtra(EXTRA_MANGASYNC)
if (manga != null) {
updateLastChapterRead(manga as MangaSync, startId)
return Service.START_REDELIVER_INTENT
} else {
stopSelf(startId)
return Service.START_NOT_STICKY
}
}
override fun onBind(intent: Intent): IBinder? {
return null
}
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
val sync = syncManager.getService(mangaSync.sync_id)
subscriptions.add(Observable.defer { sync.update(mangaSync) }
.flatMap {
if (it.isSuccessful) {
db.insertMangaSync(mangaSync).asRxObservable()
} else {
Observable.error(Exception("Could not update manga in remote service"))
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ stopSelf(startId) },
{ stopSelf(startId) }))
}
companion object {
private val EXTRA_MANGASYNC = "extra_mangasync"
@JvmStatic
fun start(context: Context, mangaSync: MangaSync) {
val intent = Intent(context, UpdateMangaSyncService::class.java)
intent.putExtra(EXTRA_MANGASYNC, mangaSync)
context.startService(intent)
}
}
}

View File

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync.base;
import eu.kanade.tachiyomi.data.database.models.MangaSync;
import okhttp3.Response;
import rx.Observable;
public abstract class MangaSyncService {
// Name of the manga sync service to display
public abstract String getName();
// Id of the sync service (must be declared and obtained from MangaSyncManager to avoid conflicts)
public abstract int getId();
public abstract Observable<Boolean> login(String username, String password);
public abstract boolean isLogged();
public abstract Observable<Response> update(MangaSync manga);
public abstract Observable<Response> add(MangaSync manga);
public abstract Observable<Response> bind(MangaSync manga);
public abstract String getStatus(int status);
}

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.data.mangasync.base
import android.content.Context
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.Response
import rx.Observable
import javax.inject.Inject
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)
}
// Name of the manga sync service to display
abstract val name: String
abstract fun login(username: String, password: String): Observable<Boolean>
open val isLogged: Boolean
get() = !preferences.getMangaSyncUsername(this).isEmpty() &&
!preferences.getMangaSyncPassword(this).isEmpty()
abstract fun update(manga: MangaSync): Observable<Response>
abstract fun add(manga: MangaSync): Observable<Response>
abstract fun bind(manga: MangaSync): Observable<Response>
abstract fun getStatus(status: Int): String
}

View File

@ -1,263 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync.services;
import android.content.Context;
import android.net.Uri;
import android.util.Xml;
import org.jsoup.Jsoup;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import javax.inject.Inject;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.MangaSync;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.network.NetworkHelper;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import okhttp3.Credentials;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.RequestBody;
import okhttp3.Response;
import rx.Observable;
public class MyAnimeList extends MangaSyncService {
@Inject PreferencesHelper preferences;
@Inject NetworkHelper networkService;
private Headers headers;
private String username;
public static final String BASE_URL = "http://myanimelist.net";
private static final String ENTRY_TAG = "entry";
private static final String CHAPTER_TAG = "chapter";
private static final String SCORE_TAG = "score";
private static final String STATUS_TAG = "status";
public static final int READING = 1;
public static final int COMPLETED = 2;
public static final int ON_HOLD = 3;
public static final int DROPPED = 4;
public static final int PLAN_TO_READ = 6;
public static final int DEFAULT_STATUS = READING;
public static final int DEFAULT_SCORE = 0;
private Context context;
public MyAnimeList(Context context) {
this.context = context;
App.get(context).getComponent().inject(this);
String username = preferences.getMangaSyncUsername(this);
String password = preferences.getMangaSyncPassword(this);
if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password);
}
}
@Override
public String getName() {
return "MyAnimeList";
}
@Override
public int getId() {
return MangaSyncManager.MYANIMELIST;
}
public String getLoginUrl() {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.toString();
}
public Observable<Boolean> login(String username, String password) {
createHeaders(username, password);
return networkService.getResponse(getLoginUrl(), headers, false)
.map(response -> response.code() == 200);
}
@Override
public boolean isLogged() {
return !preferences.getMangaSyncUsername(this).isEmpty()
&& !preferences.getMangaSyncPassword(this).isEmpty();
}
public String getSearchUrl(String query) {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString();
}
public Observable<List<MangaSync>> search(String query) {
return networkService.getStringResponse(getSearchUrl(query), headers, true)
.map(Jsoup::parse)
.flatMap(doc -> Observable.from(doc.select("entry")))
.filter(entry -> !entry.select("type").text().equals("Novel"))
.map(entry -> {
MangaSync manga = MangaSync.create(this);
manga.title = entry.select("title").first().text();
manga.remote_id = Integer.parseInt(entry.select("id").first().text());
manga.total_chapters = Integer.parseInt(entry.select("chapters").first().text());
return manga;
})
.toList();
}
public String getListUrl(String username) {
return Uri.parse(BASE_URL).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString();
}
public Observable<List<MangaSync>> getList() {
// TODO cache this list for a few minutes
return networkService.getStringResponse(getListUrl(username), headers, true)
.map(Jsoup::parse)
.flatMap(doc -> Observable.from(doc.select("manga")))
.map(entry -> {
MangaSync manga = MangaSync.create(this);
manga.title = entry.select("series_title").first().text();
manga.remote_id = Integer.parseInt(
entry.select("series_mangadb_id").first().text());
manga.last_chapter_read = Integer.parseInt(
entry.select("my_read_chapters").first().text());
manga.status = Integer.parseInt(
entry.select("my_status").first().text());
// MAL doesn't support score with decimals
manga.score = Integer.parseInt(
entry.select("my_score").first().text());
manga.total_chapters = Integer.parseInt(
entry.select("series_chapters").first().text());
return manga;
})
.toList();
}
public String getUpdateUrl(MangaSync manga) {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath(manga.remote_id + ".xml")
.toString();
}
public Observable<Response> update(MangaSync manga) {
try {
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED;
}
RequestBody payload = getMangaPostPayload(manga);
return networkService.postData(getUpdateUrl(manga), payload, headers);
} catch (IOException e) {
return Observable.error(e);
}
}
public String getAddUrl(MangaSync manga) {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath(manga.remote_id + ".xml")
.toString();
}
public Observable<Response> add(MangaSync manga) {
try {
RequestBody payload = getMangaPostPayload(manga);
return networkService.postData(getAddUrl(manga), payload, headers);
} catch (IOException e) {
return Observable.error(e);
}
}
private RequestBody getMangaPostPayload(MangaSync manga) throws IOException {
XmlSerializer xml = Xml.newSerializer();
StringWriter writer = new StringWriter();
xml.setOutput(writer);
xml.startDocument("UTF-8", false);
xml.startTag("", ENTRY_TAG);
// Last chapter read
if (manga.last_chapter_read != 0) {
xml.startTag("", CHAPTER_TAG);
xml.text(manga.last_chapter_read + "");
xml.endTag("", CHAPTER_TAG);
}
// Manga status in the list
xml.startTag("", STATUS_TAG);
xml.text(manga.status + "");
xml.endTag("", STATUS_TAG);
// Manga score
xml.startTag("", SCORE_TAG);
xml.text(manga.score + "");
xml.endTag("", SCORE_TAG);
xml.endTag("", ENTRY_TAG);
xml.endDocument();
FormBody.Builder form = new FormBody.Builder();
form.add("data", writer.toString());
return form.build();
}
public Observable<Response> bind(MangaSync manga) {
return getList()
.flatMap(list -> {
manga.sync_id = getId();
for (MangaSync remoteManga : list) {
if (remoteManga.remote_id == manga.remote_id) {
// Manga is already in the list
manga.copyPersonalFrom(remoteManga);
return update(manga);
}
}
// Set default fields if it's not found in the list
manga.score = DEFAULT_SCORE;
manga.status = DEFAULT_STATUS;
return add(manga);
});
}
@Override
public String getStatus(int status) {
switch (status) {
case READING:
return context.getString(R.string.reading);
case COMPLETED:
return context.getString(R.string.completed);
case ON_HOLD:
return context.getString(R.string.on_hold);
case DROPPED:
return context.getString(R.string.dropped);
case PLAN_TO_READ:
return context.getString(R.string.plan_to_read);
}
return "";
}
public void createHeaders(String username, String password) {
this.username = username;
Headers.Builder builder = new Headers.Builder();
builder.add("Authorization", Credentials.basic(username, password));
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C");
setHeaders(builder.build());
}
public void setHeaders(Headers headers) {
this.headers = headers;
}
}

View File

@ -0,0 +1,216 @@
package eu.kanade.tachiyomi.data.mangasync.services
import android.content.Context
import android.net.Uri
import android.util.Xml
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.post
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.*
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlSerializer
import rx.Observable
import java.io.StringWriter
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
startTag(namespace, tag)
text(body)
endTag(namespace, tag)
}
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
private lateinit var headers: Headers
private lateinit var username: String
companion object {
val BASE_URL = "http://myanimelist.net"
private val ENTRY_TAG = "entry"
private val CHAPTER_TAG = "chapter"
private val SCORE_TAG = "score"
private val STATUS_TAG = "status"
val READING = 1
val COMPLETED = 2
val ON_HOLD = 3
val DROPPED = 4
val PLAN_TO_READ = 6
val DEFAULT_STATUS = READING
val DEFAULT_SCORE = 0
}
init {
val username = preferences.getMangaSyncUsername(this)
val password = preferences.getMangaSyncPassword(this)
if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password)
}
}
override val name: String
get() = "MyAnimeList"
fun getLoginUrl(): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.toString()
}
override fun login(username: String, password: String): Observable<Boolean> {
createHeaders(username, password)
return networkService.request(get(getLoginUrl(), headers))
.map { it.code() == 200 }
}
fun getSearchUrl(query: String): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString()
}
fun search(query: String): Observable<List<MangaSync>> {
return networkService.requestBody(get(getSearchUrl(query), headers))
.map { Jsoup.parse(it) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
val manga = MangaSync.create(this)
manga.title = it.selectText("title")
manga.remote_id = it.selectInt("id")
manga.total_chapters = it.selectInt("chapters")
manga
}
.toList()
}
fun getListUrl(username: String): String {
return Uri.parse(BASE_URL).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString()
}
// MAL doesn't support score with decimals
fun getList(): Observable<List<MangaSync>> {
return networkService.requestBody(get(getListUrl(username), headers), true)
.map { Jsoup.parse(it) }
.flatMap { Observable.from(it.select("manga")) }
.map {
val manga = MangaSync.create(this)
manga.title = it.selectText("series_title")
manga.remote_id = it.selectInt("series_mangadb_id")
manga.last_chapter_read = it.selectInt("my_read_chapters")
manga.status = it.selectInt("my_status")
manga.score = it.selectInt("my_score").toFloat()
manga.total_chapters = it.selectInt("series_chapters")
manga
}
.toList()
}
fun getUpdateUrl(manga: MangaSync): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath(manga.remote_id.toString() + ".xml")
.toString()
}
override fun update(manga: MangaSync): Observable<Response> {
return Observable.defer {
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED
}
networkService.request(post(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
}
}
fun getAddUrl(manga: MangaSync): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath(manga.remote_id.toString() + ".xml")
.toString()
}
override fun add(manga: MangaSync): Observable<Response> {
return Observable.defer {
networkService.request(post(getAddUrl(manga), headers, getMangaPostPayload(manga)))
}
}
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
val xml = Xml.newSerializer()
val writer = StringWriter()
with(xml) {
setOutput(writer)
startDocument("UTF-8", false)
startTag("", ENTRY_TAG)
// Last chapter read
if (manga.last_chapter_read != 0) {
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
}
// Manga status in the list
inTag(STATUS_TAG, manga.status.toString())
// Manga score
inTag(SCORE_TAG, manga.score.toString())
endTag("", ENTRY_TAG)
endDocument()
}
val form = FormBody.Builder()
form.add("data", writer.toString())
return form.build()
}
override fun bind(manga: MangaSync): Observable<Response> {
return getList()
.flatMap {
manga.sync_id = id
for (remoteManga in it) {
if (remoteManga.remote_id == manga.remote_id) {
// Manga is already in the list
manga.copyPersonalFrom(remoteManga)
return@flatMap update(manga)
}
}
// Set default fields if it's not found in the list
manga.score = DEFAULT_SCORE.toFloat()
manga.status = DEFAULT_STATUS
return@flatMap add(manga)
}
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLAN_TO_READ -> getString(R.string.plan_to_read)
else -> ""
}
}
fun createHeaders(username: String, password: String) {
this.username = username
val builder = Headers.Builder()
builder.add("Authorization", Credentials.basic(username, password))
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
headers = builder.build()
}
}

View File

@ -1,141 +0,0 @@
package eu.kanade.tachiyomi.data.network;
import android.content.Context;
import java.io.File;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.CookieStore;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import rx.Observable;
public final class NetworkHelper {
private OkHttpClient client;
private OkHttpClient forceCacheClient;
private CookieManager cookieManager;
public final Headers NULL_HEADERS = new Headers.Builder().build();
public final RequestBody NULL_REQUEST_BODY = new FormBody.Builder().build();
public final CacheControl CACHE_CONTROL = new CacheControl.Builder()
.maxAge(10, TimeUnit.MINUTES)
.build();
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = chain -> {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.build();
};
private static final int CACHE_SIZE = 5 * 1024 * 1024; // 5 MiB
private static final String CACHE_DIR_NAME = "network_cache";
public NetworkHelper(Context context) {
File cacheDir = new File(context.getCacheDir(), CACHE_DIR_NAME);
cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
client = new OkHttpClient.Builder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.cache(new Cache(cacheDir, CACHE_SIZE))
.build();
forceCacheClient = client.newBuilder()
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.build();
}
public Observable<Response> getResponse(final String url, final Headers headers, boolean forceCache) {
return Observable.defer(() -> {
try {
OkHttpClient c = forceCache ? forceCacheClient : client;
Request request = new Request.Builder()
.url(url)
.headers(headers != null ? headers : NULL_HEADERS)
.cacheControl(CACHE_CONTROL)
.build();
return Observable.just(c.newCall(request).execute());
} catch (Throwable e) {
return Observable.error(e);
}
}).retry(1);
}
public Observable<String> mapResponseToString(final Response response) {
return Observable.defer(() -> {
try {
return Observable.just(response.body().string());
} catch (Throwable e) {
return Observable.error(e);
}
});
}
public Observable<String> getStringResponse(final String url, final Headers headers, boolean forceCache) {
return getResponse(url, headers, forceCache)
.flatMap(this::mapResponseToString);
}
public Observable<Response> postData(final String url, final RequestBody formBody, final Headers headers) {
return Observable.defer(() -> {
try {
Request request = new Request.Builder()
.url(url)
.post(formBody != null ? formBody : NULL_REQUEST_BODY)
.headers(headers != null ? headers : NULL_HEADERS)
.build();
return Observable.just(client.newCall(request).execute());
} catch (Throwable e) {
return Observable.error(e);
}
}).retry(1);
}
public Observable<Response> getProgressResponse(final String url, final Headers headers, final ProgressListener listener) {
return Observable.defer(() -> {
try {
Request request = new Request.Builder()
.url(url)
.cacheControl(CacheControl.FORCE_NETWORK)
.headers(headers != null ? headers : NULL_HEADERS)
.build();
OkHttpClient progressClient = client.newBuilder()
.cache(null)
.addNetworkInterceptor(chain -> {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new ProgressResponseBody(originalResponse.body(), listener))
.build();
}).build();
return Observable.just(progressClient.newCall(request).execute());
} catch (Throwable e) {
return Observable.error(e);
}
}).retry(1);
}
public CookieStore getCookies() {
return cookieManager.getCookieStore();
}
}

View File

@ -0,0 +1,78 @@
package eu.kanade.tachiyomi.data.network
import android.content.Context
import okhttp3.*
import rx.Observable
import java.io.File
import java.net.CookieManager
import java.net.CookiePolicy
import java.net.CookieStore
class NetworkHelper(context: Context) {
private val client: OkHttpClient
private val forceCacheClient: OkHttpClient
private val cookieManager: CookieManager
private val forceCacheInterceptor = { chain: Interceptor.Chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.build()
}
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
private val cacheDir = "network_cache"
init {
val cacheDir = File(context.cacheDir, cacheDir)
cookieManager = CookieManager()
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
client = OkHttpClient.Builder()
.cookieJar(JavaNetCookieJar(cookieManager))
.cache(Cache(cacheDir, cacheSize))
.build()
forceCacheClient = client.newBuilder()
.addNetworkInterceptor(forceCacheInterceptor)
.build()
}
@JvmOverloads
fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
return Observable.fromCallable {
val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute()
}
}
@JvmOverloads
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
return request(request, forceCache)
.map { it.body().string() }
}
fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> {
return Observable.fromCallable {
val progressClient = client.newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.build()
}
.build()
progressClient.newCall(request).execute()
}.retry(1)
}
val cookies: CookieStore
get() = cookieManager.cookieStore
}

View File

@ -1,5 +0,0 @@
package eu.kanade.tachiyomi.data.network;
public interface ProgressListener {
void update(long bytesRead, long contentLength, boolean done);
}

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.data.network
interface ProgressListener {
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
}

View File

@ -1,52 +0,0 @@
package eu.kanade.tachiyomi.data.network;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import okio.ForwardingSource;
import okio.Okio;
import okio.Source;
public class ProgressResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private final ProgressListener progressListener;
private BufferedSource bufferedSource;
public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
this.responseBody = responseBody;
this.progressListener = progressListener;
}
@Override public MediaType contentType() {
return responseBody.contentType();
}
@Override public long contentLength() {
return responseBody.contentLength();
}
@Override public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
return bytesRead;
}
};
}
}

View File

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.data.network
import okhttp3.MediaType
import okhttp3.ResponseBody
import okio.*
import java.io.IOException
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
private val bufferedSource: BufferedSource by lazy {
Okio.buffer(source(responseBody.source()))
}
override fun contentType(): MediaType {
return responseBody.contentType()
}
override fun contentLength(): Long {
return responseBody.contentLength()
}
override fun source(): BufferedSource {
return bufferedSource
}
private fun source(source: Source): Source {
return object : ForwardingSource(source) {
internal var totalBytesRead = 0L
@Throws(IOException::class)
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
return bytesRead
}
}
}
}

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.data.network
import okhttp3.*
import java.util.concurrent.TimeUnit
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, TimeUnit.MINUTES).build()
private val DEFAULT_HEADERS = Headers.Builder().build()
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
@JvmOverloads
fun get(url: String,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
return Request.Builder()
.url(url)
.headers(headers)
.cacheControl(cache)
.build()
}
@JvmOverloads
fun post(url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
return Request.Builder()
.url(url)
.post(body)
.headers(headers)
.cacheControl(cache)
.build()
}

View File

@ -1,193 +0,0 @@
package eu.kanade.tachiyomi.data.preference;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.preference.PreferenceManager;
import com.f2prateek.rx.preferences.Preference;
import com.f2prateek.rx.preferences.RxSharedPreferences;
import java.io.File;
import java.io.IOException;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.source.base.Source;
public class PreferencesHelper {
private Context context;
private SharedPreferences prefs;
private RxSharedPreferences rxPrefs;
private static final String SOURCE_ACCOUNT_USERNAME = "pref_source_username_";
private static final String SOURCE_ACCOUNT_PASSWORD = "pref_source_password_";
private static final String MANGASYNC_ACCOUNT_USERNAME = "pref_mangasync_username_";
private static final String MANGASYNC_ACCOUNT_PASSWORD = "pref_mangasync_password_";
private File defaultDownloadsDir;
public PreferencesHelper(Context context) {
this.context = context;
PreferenceManager.setDefaultValues(context, R.xml.pref_reader, false);
prefs = PreferenceManager.getDefaultSharedPreferences(context);
rxPrefs = RxSharedPreferences.create(prefs);
defaultDownloadsDir = new File(Environment.getExternalStorageDirectory() +
File.separator + context.getString(R.string.app_name), "downloads");
// Create default directory
if (getDownloadsDirectory().equals(defaultDownloadsDir.getAbsolutePath()) &&
!defaultDownloadsDir.exists()) {
defaultDownloadsDir.mkdirs();
}
// Don't display downloaded chapters in gallery apps creating a ".nomedia" file
try {
new File(getDownloadsDirectory(), ".nomedia").createNewFile();
} catch (IOException e) { /* Ignore */ }
}
private String getKey(int keyResource) {
return context.getString(keyResource);
}
public void clear() {
prefs.edit().clear().apply();
}
public Preference<Integer> rotation() {
return rxPrefs.getInteger(getKey(R.string.pref_rotation_type_key), 1);
}
public Preference<Boolean> enableTransitions() {
return rxPrefs.getBoolean(getKey(R.string.pref_enable_transitions_key), true);
}
public Preference<Boolean> showPageNumber() {
return rxPrefs.getBoolean(getKey(R.string.pref_show_page_number_key), true);
}
public Preference<Boolean> hideStatusBar() {
return rxPrefs.getBoolean(getKey(R.string.pref_hide_status_bar_key), true);
}
public Preference<Boolean> keepScreenOn() {
return rxPrefs.getBoolean(getKey(R.string.pref_keep_screen_on_key), true);
}
public Preference<Boolean> customBrightness() {
return rxPrefs.getBoolean(getKey(R.string.pref_custom_brightness_key), false);
}
public Preference<Float> customBrightnessValue() {
return rxPrefs.getFloat(getKey(R.string.pref_custom_brightness_value_key), 0F);
}
public int getDefaultViewer() {
return prefs.getInt(getKey(R.string.pref_default_viewer_key), 1);
}
public Preference<Integer> imageScaleType() {
return rxPrefs.getInteger(getKey(R.string.pref_image_scale_type_key), 1);
}
public Preference<Integer> imageDecoder() {
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0);
}
public Preference<Integer> zoomStart() {
return rxPrefs.getInteger(getKey(R.string.pref_zoom_start_key), 1);
}
public Preference<Integer> readerTheme() {
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0);
}
public Preference<Integer> portraitColumns() {
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_portrait_key), 0);
}
public Preference<Integer> landscapeColumns() {
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_landscape_key), 0);
}
public boolean updateOnlyNonCompleted() {
return prefs.getBoolean(getKey(R.string.pref_update_only_non_completed_key), false);
}
public boolean autoUpdateMangaSync() {
return prefs.getBoolean(getKey(R.string.pref_auto_update_manga_sync_key), true);
}
public boolean askUpdateMangaSync() {
return prefs.getBoolean(getKey(R.string.pref_ask_update_manga_sync_key), false);
}
public Preference<Integer> lastUsedCatalogueSource() {
return rxPrefs.getInteger(getKey(R.string.pref_last_catalogue_source_key), -1);
}
public boolean seamlessMode() {
return prefs.getBoolean(getKey(R.string.pref_seamless_mode_key), true);
}
public Preference<Boolean> catalogueAsList() {
return rxPrefs.getBoolean(getKey(R.string.pref_display_catalogue_as_list), false);
}
public String getSourceUsername(Source source) {
return prefs.getString(SOURCE_ACCOUNT_USERNAME + source.getId(), "");
}
public String getSourcePassword(Source source) {
return prefs.getString(SOURCE_ACCOUNT_PASSWORD + source.getId(), "");
}
public void setSourceCredentials(Source source, String username, String password) {
prefs.edit()
.putString(SOURCE_ACCOUNT_USERNAME + source.getId(), username)
.putString(SOURCE_ACCOUNT_PASSWORD + source.getId(), password)
.apply();
}
public String getMangaSyncUsername(MangaSyncService sync) {
return prefs.getString(MANGASYNC_ACCOUNT_USERNAME + sync.getId(), "");
}
public String getMangaSyncPassword(MangaSyncService sync) {
return prefs.getString(MANGASYNC_ACCOUNT_PASSWORD + sync.getId(), "");
}
public void setMangaSyncCredentials(MangaSyncService sync, String username, String password) {
prefs.edit()
.putString(MANGASYNC_ACCOUNT_USERNAME + sync.getId(), username)
.putString(MANGASYNC_ACCOUNT_PASSWORD + sync.getId(), password)
.apply();
}
public String getDownloadsDirectory() {
return prefs.getString(getKey(R.string.pref_download_directory_key),
defaultDownloadsDir.getAbsolutePath());
}
public void setDownloadsDirectory(String path) {
prefs.edit().putString(getKey(R.string.pref_download_directory_key), path).apply();
}
public Preference<Integer> downloadThreads() {
return rxPrefs.getInteger(getKey(R.string.pref_download_slots_key), 1);
}
public boolean downloadOnlyOverWifi() {
return prefs.getBoolean(getKey(R.string.pref_download_only_over_wifi_key), true);
}
public static int getLibraryUpdateInterval(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_library_update_interval_key), 0);
}
}

View File

@ -0,0 +1,216 @@
package eu.kanade.tachiyomi.data.preference
import android.content.Context
import android.os.Environment
import android.preference.PreferenceManager
import com.f2prateek.rx.preferences.Preference
import com.f2prateek.rx.preferences.RxSharedPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.source.base.Source
import java.io.File
import java.io.IOException
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
class PreferencesHelper(private val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val rxPrefs = RxSharedPreferences.create(prefs)
private val defaultDownloadsDir: File
init {
defaultDownloadsDir = File(Environment.getExternalStorageDirectory().absolutePath +
File.separator + context.getString(R.string.app_name), "downloads")
// Don't display downloaded chapters in gallery apps creating a ".nomedia" file
try {
File(downloadsDirectory, ".nomedia").createNewFile()
} catch (e: IOException) {
/* Ignore */
}
}
companion object {
const val SOURCE_ACCOUNT_USERNAME = "pref_source_username_"
const val SOURCE_ACCOUNT_PASSWORD = "pref_source_password_"
const val MANGASYNC_ACCOUNT_USERNAME = "pref_mangasync_username_"
const val MANGASYNC_ACCOUNT_PASSWORD = "pref_mangasync_password_"
fun getLibraryUpdateInterval(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_library_update_interval_key), 0)
}
@JvmStatic
fun getTheme(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_theme_key), 1)
}
}
private fun getKey(keyResource: Int): String {
return context.getString(keyResource)
}
fun clear() {
prefs.edit().clear().apply()
}
fun rotation(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_rotation_type_key), 1)
}
fun enableTransitions(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_enable_transitions_key), true)
}
fun showPageNumber(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_show_page_number_key), true)
}
fun hideStatusBar(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_hide_status_bar_key), true)
}
fun keepScreenOn(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_keep_screen_on_key), true)
}
fun customBrightness(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_custom_brightness_key), false)
}
fun customBrightnessValue(): Preference<Float> {
return rxPrefs.getFloat(getKey(R.string.pref_custom_brightness_value_key), 0f)
}
val defaultViewer: Int
get() = prefs.getInt(getKey(R.string.pref_default_viewer_key), 1)
fun imageScaleType(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_image_scale_type_key), 1)
}
fun imageDecoder(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0)
}
fun zoomStart(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_zoom_start_key), 1)
}
fun readerTheme(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0)
}
fun portraitColumns(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_portrait_key), 0)
}
fun landscapeColumns(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_landscape_key), 0)
}
fun updateOnlyNonCompleted(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_update_only_non_completed_key), false)
}
fun autoUpdateMangaSync(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_auto_update_manga_sync_key), true)
}
fun askUpdateMangaSync(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_ask_update_manga_sync_key), false)
}
fun lastUsedCatalogueSource(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_last_catalogue_source_key), -1)
}
fun seamlessMode(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_seamless_mode_key), true)
}
fun catalogueAsList(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_display_catalogue_as_list), false)
}
fun enabledLanguages(): Preference<MutableSet<String>> {
return rxPrefs.getStringSet(getKey(R.string.pref_source_languages), setOf("EN"))
}
fun getSourceUsername(source: Source): String {
return prefs.getString(SOURCE_ACCOUNT_USERNAME + source.id, "")
}
fun getSourcePassword(source: Source): String {
return prefs.getString(SOURCE_ACCOUNT_PASSWORD + source.id, "")
}
fun setSourceCredentials(source: Source, username: String, password: String) {
prefs.edit()
.putString(SOURCE_ACCOUNT_USERNAME + source.id, username)
.putString(SOURCE_ACCOUNT_PASSWORD + source.id, password)
.apply()
}
fun getMangaSyncUsername(sync: MangaSyncService): String {
return prefs.getString(MANGASYNC_ACCOUNT_USERNAME + sync.id, "")
}
fun getMangaSyncPassword(sync: MangaSyncService): String {
return prefs.getString(MANGASYNC_ACCOUNT_PASSWORD + sync.id, "")
}
fun setMangaSyncCredentials(sync: MangaSyncService, username: String, password: String) {
prefs.edit()
.putString(MANGASYNC_ACCOUNT_USERNAME + sync.id, username)
.putString(MANGASYNC_ACCOUNT_PASSWORD + sync.id, password)
.apply()
}
var downloadsDirectory: String
get() = prefs.getString(getKey(R.string.pref_download_directory_key), defaultDownloadsDir.absolutePath)
set(path) = prefs.edit().putString(getKey(R.string.pref_download_directory_key), path).apply()
fun downloadThreads(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_download_slots_key), 1)
}
fun downloadOnlyOverWifi(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_download_only_over_wifi_key), true)
}
fun removeAfterRead(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_remove_after_read_key), false)
}
fun removeAfterReadPrevious(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_remove_after_read_previous_key), false)
}
fun removeAfterMarkedAsRead(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_remove_after_marked_as_read_key), false)
}
fun updateOnlyWhenCharging(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_update_only_when_charging_key), false)
}
fun libraryUpdateInterval(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_library_update_interval_key), 0)
}
fun filterDownloaded(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_filter_downloaded_key), false)
}
fun filterUnread(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_filter_unread_key), false)
}
}

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.data.rest;
import retrofit.http.GET;
import rx.Observable;
/**
* Used to connect with the Github API
*/
public interface GithubService {
String SERVICE_ENDPOINT = "https://api.github.com";
@GET("/repos/inorichi/tachiyomi/releases/latest") Observable<Release> getLatestVersion();
}

View File

@ -1,93 +0,0 @@
package eu.kanade.tachiyomi.data.rest;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
* Release object
* Contains information about the latest release
*/
public class Release {
/**
* Version name V0.0.0
*/
@SerializedName("tag_name")
private final String version;
/** Change Log */
@SerializedName("body")
private final String log;
/** Assets containing download url */
@SerializedName("assets")
private final List<Assets> assets;
/**
* Release constructor
*
* @param version version of latest release
* @param log log of latest release
* @param assets assets of latest release
*/
public Release(String version, String log, List<Assets> assets) {
this.version = version;
this.log = log;
this.assets = assets;
}
/**
* Get latest release version
*
* @return latest release version
*/
public String getVersion() {
return version;
}
/**
* Get change log of latest release
*
* @return change log of latest release
*/
public String getChangeLog() {
return log;
}
/**
* Get download link of latest release
*
* @return download link of latest release
*/
public String getDownloadLink() {
return assets.get(0).getDownloadLink();
}
/**
* Assets class containing download url
*/
class Assets {
@SerializedName("browser_download_url")
private final String download_url;
/**
* Assets Constructor
*
* @param download_url download url
*/
@SuppressWarnings("unused") public Assets(String download_url) {
this.download_url = download_url;
}
/**
* Get download link of latest release
*
* @return download link of latest release
*/
public String getDownloadLink() {
return download_url;
}
}
}

View File

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.data.rest;
import retrofit.RestAdapter;
public class ServiceFactory {
/**
* Creates a retrofit service from an arbitrary class (clazz)
*
* @param clazz Java interface of the retrofit service
* @param endPoint REST endpoint url
* @return retrofit service with defined endpoint
*/
public static <T> T createRetrofitService(final Class<T> clazz, final String endPoint) {
final RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(endPoint)
.build();
return restAdapter.create(clazz);
}
}

View File

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.data.source
class Language(val lang: String, val code: String)
val EN = Language("English", "EN")
val RU = Language("Russian", "RU")
fun getLanguages(): List<Language> = listOf(EN, RU)

View File

@ -1,68 +0,0 @@
package eu.kanade.tachiyomi.data.source;
import android.content.Context;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.online.english.Batoto;
import eu.kanade.tachiyomi.data.source.online.english.Kissmanga;
import eu.kanade.tachiyomi.data.source.online.english.Mangafox;
import eu.kanade.tachiyomi.data.source.online.english.Mangahere;
public class SourceManager {
public static final int BATOTO = 1;
public static final int MANGAHERE = 2;
public static final int MANGAFOX = 3;
public static final int KISSMANGA = 4;
private HashMap<Integer, Source> sourcesMap;
private Context context;
public SourceManager(Context context) {
sourcesMap = new HashMap<>();
this.context = context;
initializeSources();
}
public Source get(int sourceKey) {
if (!sourcesMap.containsKey(sourceKey)) {
sourcesMap.put(sourceKey, createSource(sourceKey));
}
return sourcesMap.get(sourceKey);
}
private Source createSource(int sourceKey) {
switch (sourceKey) {
case BATOTO:
return new Batoto(context);
case MANGAHERE:
return new Mangahere(context);
case MANGAFOX:
return new Mangafox(context);
case KISSMANGA:
return new Kissmanga(context);
}
return null;
}
private void initializeSources() {
sourcesMap.put(BATOTO, createSource(BATOTO));
sourcesMap.put(MANGAHERE, createSource(MANGAHERE));
sourcesMap.put(MANGAFOX, createSource(MANGAFOX));
sourcesMap.put(KISSMANGA, createSource(KISSMANGA));
}
public List<Source> getSources() {
List<Source> sources = new ArrayList<>(sourcesMap.values());
Collections.sort(sources, (s1, s2) -> s1.getName().compareTo(s2.getName()));
return sources;
}
}

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.data.source
import android.content.Context
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.online.english.Batoto
import eu.kanade.tachiyomi.data.source.online.english.Kissmanga
import eu.kanade.tachiyomi.data.source.online.english.Mangafox
import eu.kanade.tachiyomi.data.source.online.english.Mangahere
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;
import eu.kanade.tachiyomi.data.source.online.english.ReadMangaToday
import java.util.*
open class SourceManager(private val context: Context) {
val sourcesMap: HashMap<Int, Source>
val BATOTO = 1
val MANGAHERE = 2
val MANGAFOX = 3
val KISSMANGA = 4
val READMANGA = 5
val MINTMANGA = 6
val MANGACHAN = 7
val READMANGATODAY = 8
val LAST_SOURCE = 8
init {
sourcesMap = createSourcesMap()
}
open fun get(sourceKey: Int): Source? {
return sourcesMap[sourceKey]
}
private fun createSource(sourceKey: Int): Source? = when (sourceKey) {
BATOTO -> Batoto(context)
MANGAHERE -> Mangahere(context)
MANGAFOX -> Mangafox(context)
KISSMANGA -> Kissmanga(context)
READMANGA -> Readmanga(context)
MINTMANGA -> Mintmanga(context)
MANGACHAN -> Mangachan(context)
READMANGATODAY -> ReadMangaToday(context)
else -> null
}
private fun createSourcesMap(): HashMap<Int, Source> {
val map = HashMap<Int, Source>()
for (i in 1..LAST_SOURCE) {
val source = createSource(i)
if (source != null) {
source.id = i
map.put(i, source)
}
}
return map
}
fun getSources(): List<Source> = ArrayList(sourcesMap.values)
}

View File

@ -6,6 +6,7 @@ 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.Language;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import okhttp3.Headers;
import okhttp3.Response;
@ -13,11 +14,26 @@ import rx.Observable;
public abstract class BaseSource {
private int id;
// Id of the source
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public abstract Language getLang();
// Name of the source to display
public abstract String getName();
// Id of the source (must be declared and obtained from SourceManager to avoid conflicts)
public abstract int getId();
// Name of the source to display with the language
public String getVisibleName() {
return getName() + " (" + getLang().getCode() + ")";
}
// Base url of the source, like: http://example.com
public abstract String getBaseUrl();
@ -68,24 +84,6 @@ public abstract class BaseSource {
protected boolean isAuthenticationSuccessful(Response response) {
throw new UnsupportedOperationException("Not implemented");
}
// Default fields, they can be overriden by sources' implementation
// Get the URL to the details of a manga, useful if the source provides some kind of API or fast calls
protected String overrideMangaUrl(String defaultMangaUrl) {
return defaultMangaUrl;
}
// Get the URL of the first page that contains a source image and the page list
protected String overrideChapterUrl(String defaultPageUrl) {
return defaultPageUrl;
}
// Get the URL of the pages that contains source images
protected String overridePageUrl(String defaultPageUrl) {
return defaultPageUrl;
}
// Default headers, it can be overriden by children or just add new keys
protected Headers.Builder headersBuilder() {
@ -96,6 +94,6 @@ public abstract class BaseSource {
@Override
public String toString() {
return getName();
return getVisibleName();
}
}

View File

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

View File

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

View File

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

View File

@ -27,25 +27,29 @@ import java.util.regex.Pattern;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.network.ReqKt;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.LoginSource;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Request;
import okhttp3.Response;
import rx.Observable;
import rx.functions.Func1;
public class Batoto extends LoginSource {
public static final String NAME = "Batoto (EN)";
public static final String NAME = "Batoto";
public static final String BASE_URL = "http://bato.to";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/search_ajax?order_cond=views&order=desc&p=%d";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/search_ajax?order_cond=views&order=desc&p=%s";
public static final String SEARCH_URL = BASE_URL + "/search_ajax?name=%s&p=%s";
public static final String CHAPTER_URL = "/areader?id=%s&p=1";
public static final String CHAPTER_URL = BASE_URL + "/areader?id=%s&p=1";
public static final String PAGE_URL = BASE_URL + "/areader?id=%s&p=%s";
public static final String MANGA_URL = "/comic_pop?id=%s";
public static final String MANGA_URL = BASE_URL + "/comic_pop?id=%s";
public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global&section=login";
public static final Pattern staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE);
@ -73,16 +77,15 @@ public class Batoto extends LoginSource {
return NAME;
}
@Override
public int getId() {
return SourceManager.BATOTO;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
public Language getLang() {
return LanguageKt.getEN();
}
@Override
protected Headers.Builder headersBuilder() {
Headers.Builder builder = super.headersBuilder();
@ -102,23 +105,24 @@ public class Batoto extends LoginSource {
}
@Override
protected String overrideMangaUrl(String defaultMangaUrl) {
String mangaId = defaultMangaUrl.substring(defaultMangaUrl.lastIndexOf("r") + 1);
return String.format(MANGA_URL, mangaId);
protected Request mangaDetailsRequest(String mangaUrl) {
String mangaId = mangaUrl.substring(mangaUrl.lastIndexOf("r") + 1);
return ReqKt.get(String.format(MANGA_URL, mangaId), getRequestHeaders());
}
@Override
protected String overrideChapterUrl(String defaultPageUrl) {
String id = defaultPageUrl.substring(defaultPageUrl.indexOf("#") + 1);
return String.format(CHAPTER_URL, id);
protected Request pageListRequest(String pageUrl) {
String id = pageUrl.substring(pageUrl.indexOf("#") + 1);
return ReqKt.get(String.format(CHAPTER_URL, id), getRequestHeaders());
}
@Override
protected String overridePageUrl(String defaultPageUrl) {
int start = defaultPageUrl.indexOf("#") + 1;
int end = defaultPageUrl.indexOf("_", start);
String id = defaultPageUrl.substring(start, end);
return String.format(PAGE_URL, id, defaultPageUrl.substring(end+1));
protected Request imageUrlRequest(Page page) {
String pageUrl = page.getUrl();
int start = pageUrl.indexOf("#") + 1;
int end = pageUrl.indexOf("_", start);
String id = pageUrl.substring(start, end);
return ReqKt.get(String.format(PAGE_URL, id, pageUrl.substring(end+1)), getRequestHeaders());
}
private List<Manga> parseMangasFromHtml(Document parsedHtml) {
@ -290,7 +294,7 @@ public class Batoto extends LoginSource {
}
@Override
protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
if (!unparsedHtml.contains("Want to see this chapter per page instead?")) {
String firstImage = parseHtmlToImageUrl(unparsedHtml);
pages.get(0).setImageUrl(firstImage);
@ -302,7 +306,7 @@ public class Batoto extends LoginSource {
pages.get(i).setImageUrl(imageUrls.get(i).attr("src"));
}
}
return pages;
return (List<Page>) pages;
}
@Override
@ -317,10 +321,16 @@ public class Batoto extends LoginSource {
}
@Override
public Observable<Boolean> login(String username, String password) {
return networkService.getStringResponse(LOGIN_URL, requestHeaders, false)
.flatMap(response -> doLogin(response, username, password))
.map(this::isAuthenticationSuccessful);
public Observable<Boolean> login(final String username, final String password) {
return getNetworkService().requestBody(ReqKt.get(LOGIN_URL, getRequestHeaders()))
.flatMap(new Func1<String, Observable<Response>>() {
@Override
public Observable<Response> call(String response) {return doLogin(response, username, password);}
})
.map(new Func1<Response, Boolean>() {
@Override
public Boolean call(Response resp) {return isAuthenticationSuccessful(resp);}
});
}
private Observable<Response> doLogin(String response, String username, String password) {
@ -337,7 +347,7 @@ public class Batoto extends LoginSource {
formBody.add("invisible", "1");
formBody.add("rememberMe", "1");
return networkService.postData(postUrl, formBody.build(), requestHeaders);
return getNetworkService().request(ReqKt.post(postUrl, getRequestHeaders(), formBody.build()));
}
@Override
@ -348,7 +358,7 @@ public class Batoto extends LoginSource {
@Override
public boolean isLogged() {
try {
for ( HttpCookie cookie : networkService.getCookies().get(new URI(BASE_URL)) ) {
for ( HttpCookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL)) ) {
if (cookie.getName().equals("pass_hash"))
return true;
}
@ -360,16 +370,19 @@ public class Batoto extends LoginSource {
}
@Override
public Observable<List<Chapter>> pullChaptersFromNetwork(String mangaUrl) {
public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
Observable<List<Chapter>> observable;
String username = prefs.getSourceUsername(this);
String password = prefs.getSourcePassword(this);
String username = getPrefs().getSourceUsername(this);
String password = getPrefs().getSourcePassword(this);
if (username.isEmpty() && password.isEmpty()) {
observable = Observable.error(new Exception("User not logged"));
}
else if (!isLogged()) {
observable = login(username, password)
.flatMap(result -> super.pullChaptersFromNetwork(mangaUrl));
.flatMap(new Func1<Boolean, Observable<? extends List<Chapter>>>() {
@Override
public Observable<? extends List<Chapter>> call(Boolean result) {return Batoto.super.pullChaptersFromNetwork(mangaUrl);}
});
}
else {
observable = super.pullChaptersFromNetwork(mangaUrl);

View File

@ -17,19 +17,20 @@ import java.util.regex.Pattern;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.network.ReqKt;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable;
import okhttp3.Request;
public class Kissmanga extends Source {
public static final String NAME = "Kissmanga (EN)";
public static final String NAME = "Kissmanga";
public static final String HOST = "kissmanga.com";
public static final String IP = "93.174.95.110";
public static final String BASE_URL = "http://" + IP;
@ -52,16 +53,15 @@ public class Kissmanga extends Source {
return NAME;
}
@Override
public int getId() {
return SourceManager.KISSMANGA;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
public Language getLang() {
return LanguageKt.getEN();
}
@Override
protected String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, 1);
@ -72,6 +72,31 @@ public class Kissmanga extends Source {
return SEARCH_URL;
}
@Override
protected Request searchMangaRequest(MangasPage page, String query) {
if (page.page == 1) {
page.url = getInitialSearchUrl(query);
}
FormBody.Builder form = new FormBody.Builder();
form.add("authorArtist", "");
form.add("mangaName", query);
form.add("status", "");
form.add("genres", "");
return ReqKt.post(page.url, getRequestHeaders(), form.build());
}
@Override
protected Request pageListRequest(String chapterUrl) {
return ReqKt.post(getBaseUrl() + chapterUrl, getRequestHeaders());
}
@Override
protected Request imageRequest(Page page) {
return ReqKt.get(page.getImageUrl());
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
@ -104,25 +129,6 @@ public class Kissmanga extends Source {
return path != null ? BASE_URL + path : null;
}
public Observable<MangasPage> searchMangasFromNetwork(MangasPage page, String query) {
if (page.page == 1)
page.url = getInitialSearchUrl(query);
FormBody.Builder form = new FormBody.Builder();
form.add("authorArtist", "");
form.add("mangaName", query);
form.add("status", "");
form.add("genres", "");
return networkService
.postData(page.url, form.build(), requestHeaders)
.flatMap(networkService::mapResponseToString)
.map(Jsoup::parse)
.doOnNext(doc -> page.mangas = parseSearchFromHtml(doc))
.doOnNext(doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query))
.map(response -> page);
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
@ -195,19 +201,6 @@ public class Kissmanga extends Source {
return chapter;
}
@Override
public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
return networkService
.postData(getBaseUrl() + overrideChapterUrl(chapterUrl), null, requestHeaders)
.flatMap(networkService::mapResponseToString)
.flatMap(unparsedHtml -> {
List<Page> pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
return !pages.isEmpty() ?
Observable.just(parseFirstPage(pages, unparsedHtml)) :
Observable.error(new Exception("Page list is empty"));
});
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
@ -222,7 +215,7 @@ public class Kissmanga extends Source {
}
@Override
protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
Pattern p = Pattern.compile("lstImages.push\\(\"(.+?)\"");
Matcher m = p.matcher(unparsedHtml);
@ -230,7 +223,7 @@ public class Kissmanga extends Source {
while (m.find()) {
pages.get(i++).setImageUrl(m.group(1));
}
return pages;
return (List<Page>) pages;
}
@Override
@ -238,9 +231,4 @@ public class Kissmanga extends Source {
return null;
}
@Override
public Observable<Response> getImageProgressResponse(final Page page) {
return networkService.getProgressResponse(page.getImageUrl(), null, page);
}
}

View File

@ -18,14 +18,15 @@ import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.util.Parser;
public class Mangafox extends Source {
public static final String NAME = "Mangafox (EN)";
public static final String NAME = "Mangafox";
public static final String BASE_URL = "http://mangafox.me";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
public static final String SEARCH_URL =
@ -40,16 +41,15 @@ public class Mangafox extends Source {
return NAME;
}
@Override
public int getId() {
return SourceManager.MANGAFOX;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
public Language getLang() {
return LanguageKt.getEN();
}
@Override
protected String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, "");

View File

@ -18,14 +18,15 @@ import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.util.Parser;
public class Mangahere extends Source {
public static final String NAME = "Mangahere (EN)";
public static final String NAME = "Mangahere";
public static final String BASE_URL = "http://www.mangahere.co";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
public static final String SEARCH_URL = BASE_URL + "/search.php?name=%s&page=%s&sort=views&order=za";
@ -39,16 +40,15 @@ public class Mangahere extends Source {
return NAME;
}
@Override
public int getId() {
return SourceManager.MANGAHERE;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
public Language getLang() {
return LanguageKt.getEN();
}
@Override
protected String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, "");

View File

@ -0,0 +1,290 @@
package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context;
import android.net.Uri;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.Calendar;
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.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.Headers;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func1;
public class ReadMangaToday extends Source {
public static final String NAME = "ReadMangaToday";
public static final String BASE_URL = "http://www.readmanga.today";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/hot-manga/%s";
public static final String SEARCH_URL = BASE_URL + "/service/search?q=%s";
private static JsonParser parser = new JsonParser();
private static Gson gson = new Gson();
public ReadMangaToday(Context context) {
super(context);
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
protected String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, "");
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query), 1);
}
@Override
public Language getLang() {
return LanguageKt.getEN();
}
@Override
public List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div.hot-manga > div.style-list > div.box")) {
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
mangaList.add(currentManga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = Parser.element(htmlBlock, "div.title > h2 > a");
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.attr("title");
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
Element next = Parser.element(parsedHtml, "div.hot-manga > ul.pagination > li > a:contains(»)");
return next != null ? next.attr("href") : null;
}
@Override
public Observable<MangasPage> searchMangasFromNetwork(final MangasPage page, String query) {
return networkService
.requestBody(searchMangaRequest(page, query), true)
.doOnNext(new Action1<String>() {
@Override
public void call(String doc) {
page.mangas = ReadMangaToday.this.parseSearchFromJson(doc);
}
})
.map(new Func1<String, MangasPage>() {
@Override
public MangasPage call(String response) {
return page;
}
});
}
@Override
protected Headers.Builder headersBuilder() {
return super.headersBuilder().add("X-Requested-With", "XMLHttpRequest");
}
protected List<Manga> parseSearchFromJson(String unparsedJson) {
List<Manga> mangaList = new ArrayList<>();
JsonArray mangasArray = parser.parse(unparsedJson).getAsJsonArray();
for (JsonElement mangaElement : mangasArray) {
Manga currentManga = constructSearchMangaFromJsonObject(mangaElement.getAsJsonObject());
mangaList.add(currentManga);
}
return mangaList;
}
private Manga constructSearchMangaFromJsonObject(JsonObject jsonObject) {
Manga manga = new Manga();
manga.source = getId();
manga.setUrl(gson.fromJson(jsonObject.get("url"), String.class));
manga.title = gson.fromJson(jsonObject.get("title"), String.class);
return manga;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return null;
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
public Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
Element detailElement = parsedDocument.select("div.movie-meta").first();
Manga manga = Manga.create(mangaUrl);
for (Element castHtmlBlock : parsedDocument.select("div.cast ul.cast-list > li")) {
String name = Parser.text(castHtmlBlock, "ul > li > a");
String role = Parser.text(castHtmlBlock, "ul > li:eq(1)");
if (role.equals("Author")) {
manga.author = name;
} else if (role.equals("Artist")) {
manga.artist = name;
}
}
String description = Parser.text(detailElement, "li.movie-detail");
if (description != null) {
manga.description = description;
}
String genres = Parser.text(detailElement, "dl.dl-horizontal > dd:eq(5)");
if (genres != null) {
manga.genre = genres;
}
manga.status = parseStatus(Parser.text(detailElement, "dl.dl-horizontal > dd:eq(3)"));
manga.thumbnail_url = Parser.src(detailElement, "img.img-responsive");
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("Ongoing")) {
return Manga.ONGOING;
} else if (status.contains("Completed")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
public List<Chapter> parseHtmlToChapters(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("ul.chp_lst > li")) {
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(currentChapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = chapterElement.select("a").first();
Element dateElement = chapterElement.select("span.dte").first();
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href"));
chapter.name = urlElement.select("span.val").text();
}
if (dateElement != null) {
chapter.date_upload = parseDateFromElement(dateElement);
}
return chapter;
}
private long parseDateFromElement(Element dateElement) {
String dateAsString = dateElement.text();
String[] dateWords = dateAsString.split(" ");
if (dateWords.length == 3) {
int timeAgo = Integer.parseInt(dateWords[0]);
Calendar date = Calendar.getInstance();
if (dateWords[1].contains("Minute")) {
date.add(Calendar.MINUTE, - timeAgo);
} else if (dateWords[1].contains("Hour")) {
date.add(Calendar.HOUR_OF_DAY, - timeAgo);
} else if (dateWords[1].contains("Day")) {
date.add(Calendar.DAY_OF_YEAR, -timeAgo);
} else if (dateWords[1].contains("Week")) {
date.add(Calendar.WEEK_OF_YEAR, -timeAgo);
} else if (dateWords[1].contains("Month")) {
date.add(Calendar.MONTH, -timeAgo);
} else if (dateWords[1].contains("Year")) {
date.add(Calendar.YEAR, -timeAgo);
}
return date.getTimeInMillis();
}
return 0;
}
@Override
public List<String> parseHtmlToPageUrls(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
List<String> pageUrlList = new ArrayList<>();
Elements pageUrlElements = parsedDocument.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option");
for (Element pageUrlElement : pageUrlElements) {
pageUrlList.add(pageUrlElement.attr("value"));
}
return pageUrlList;
}
@Override
public String parseHtmlToImageUrl(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
Element imageElement = Parser.element(parsedDocument, "img.img-responsive-2");
return imageElement.attr("src");
}
}

View File

@ -0,0 +1,240 @@
package eu.kanade.tachiyomi.data.source.online.russian;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
public class Mangachan extends Source {
public static final String NAME = "Mangachan";
public static final String BASE_URL = "http://mangachan.ru";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/mostfavorites";
public static final String SEARCH_URL = BASE_URL + "/?do=search&subaction=search&story=%s";
public Mangachan(Context context) {
super(context);
}
@Override
public Language getLang() {
return LanguageKt.getRU();
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
protected String getInitialPopularMangasUrl() {
return POPULAR_MANGAS_URL;
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query));
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div.content_row")) {
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
mangaList.add(manga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = currentHtmlBlock.getElementsByTag("h2").select("a").first();
Element imgElement = currentHtmlBlock.getElementsByClass("manga_images").select("img").first();
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
if (imgElement != null) {
manga.thumbnail_url = BASE_URL + imgElement.attr("src");
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
String path = Parser.href(parsedHtml, "a:contains(Вперед)");
return path != null ? POPULAR_MANGAS_URL + path : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element infoElement = parsedDocument.getElementsByClass("mangatitle").first();
String description = parsedDocument.getElementById("description").text();
Manga manga = Manga.create(mangaUrl);
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 = description.replaceAll("Прислать описание", "");
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("перевод продолжается")) {
return Manga.ONGOING;
} else if (status.contains("перевод завершен")) {
return Manga.COMPLETED;
} else return Manga.UNKNOWN;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("table.table_cha tr:gt(1)")) {
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(chapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = chapterElement.select("a").first();
String date = Parser.text(chapterElement, "div.date");
if (urlElement != null) {
chapter.name = urlElement.text();
chapter.url = urlElement.attr("href");
}
if (date != null) {
try {
chapter.date_upload = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse(date).getTime();
} catch (ParseException e) { /* Ignore */ }
}
return chapter;
}
// Without this extra chapters are in the wrong place in the list
@Override
public void parseChapterNumber(Chapter chapter) {
// For chapters with url like /online/254903-fairy-tail_v56_ch474.html
String url = chapter.url.replace(".html", "");
Pattern pattern = Pattern.compile("\\d+_ch[\\d.]+");
Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
String[] parts = matcher.group().split("_ch");
chapter.chapter_number = Float.parseFloat(parts[0] + "." + AddZero(parts[1]));
} else { // For chapters with url like /online/61216-3298.html
String name = chapter.name;
name = name.replaceAll("[\\s\\d\\w\\W]+v", "");
String volume = name.substring(0, name.indexOf(" - "));
String[] parts = name.replaceFirst("\\d+ - ", "").split(" ");
chapter.chapter_number = Float.parseFloat(volume + "." + AddZero(parts[0]));
}
}
private String AddZero(String num) {
if (Float.parseFloat(num) < 1000f) {
num = "0" + num.replace(".", "");
}
if (Float.parseFloat(num) < 100f) {
num = "0" + num.replace(".", "");
}
if (Float.parseFloat(num) < 10f) {
num = "0" + num.replace(".", "");
}
return num;
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
ArrayList<String> pages = new ArrayList<>();
int beginIndex = unparsedHtml.indexOf("fullimg\":[");
int endIndex = unparsedHtml.indexOf("]", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 10, endIndex);
trimmedHtml = trimmedHtml.replaceAll("\"", "");
String[] pageUrls = trimmedHtml.split(",");
for (int i = 0; i < pageUrls.length; i++) {
pages.add("");
}
return pages;
}
@Override
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("fullimg\":[");
int endIndex = unparsedHtml.indexOf("]", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 10, endIndex);
trimmedHtml = trimmedHtml.replaceAll("\"", "");
String[] pageUrls = trimmedHtml.split(",");
for (int i = 0; i < pageUrls.length; i++) {
pages.get(i).setImageUrl(pageUrls[i].replaceAll("im.?\\.", ""));
}
return (List<Page>) pages;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}

View File

@ -0,0 +1,225 @@
package eu.kanade.tachiyomi.data.source.online.russian;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
public class Mintmanga extends Source {
public static final String NAME = "Mintmanga";
public static final String BASE_URL = "http://mintmanga.com";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/list?sortType=rate";
public static final String SEARCH_URL = BASE_URL + "/search?q=%s";
public Mintmanga(Context context) {
super(context);
}
@Override
public Language getLang() {
return LanguageKt.getRU();
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
protected String getInitialPopularMangasUrl() {
return POPULAR_MANGAS_URL;
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query));
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div.desc")) {
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
mangaList.add(manga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = currentHtmlBlock.getElementsByTag("h3").select("a").first();
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
String path = Parser.href(parsedHtml, "a:contains(→)");
return path != null ? BASE_URL + path : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element infoElement = parsedDocument.select("div.leftContent").first();
Manga manga = Manga.create(mangaUrl);
manga.title = Parser.text(infoElement, "span.eng-name");
manga.author = Parser.text(infoElement, "span.elem_author ");
manga.genre = Parser.allText(infoElement, "span.elem_genre ").replaceAll(" ,", ",");
manga.description = Parser.allText(infoElement, "div.manga-description");
if (Parser.text(infoElement, "h1.names:contains(Сингл)") != null) {
manga.status = Manga.COMPLETED;
} else {
manga.status = parseStatus(Parser.text(infoElement, "p:has(b:contains(Перевод:))"));
}
String thumbnail = Parser.element(infoElement, "img").attr("data-full");
if (thumbnail != null) {
manga.thumbnail_url = thumbnail;
}
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("продолжается")) {
return Manga.ONGOING;
}
if (status.contains("завершен")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("div.chapters-link tbody tr")) {
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(chapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = Parser.element(chapterElement, "a");
String date = Parser.text(chapterElement, "td:eq(1)");
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href") + "?mature=1");
chapter.name = urlElement.text().replaceAll(" новое", "");
}
if (date != null) {
try {
chapter.date_upload = new SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).getTime();
} catch (ParseException e) { /* Ignore */ }
}
return chapter;
}
// Without this extra chapters are in the wrong place in the list
@Override
public void parseChapterNumber(Chapter chapter) {
String url = chapter.url.replace("?mature=1", "");
String[] urlParts = url.replaceAll("/[\\w\\d]+/vol", "").split("/");
if (Float.parseFloat(urlParts[1]) < 1000f) {
urlParts[1] = "0" + urlParts[1];
}
if (Float.parseFloat(urlParts[1]) < 100f) {
urlParts[1] = "0" + urlParts[1];
}
if (Float.parseFloat(urlParts[1]) < 10f) {
urlParts[1] = "0" + urlParts[1];
}
chapter.chapter_number = Float.parseFloat(urlParts[0] + "." + urlParts[1]);
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
ArrayList<String> pages = new ArrayList<>();
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
String[] pageUrls = trimmedHtml.split("],\\[");
for (int i = 0; i < pageUrls.length; i++) {
pages.add("");
}
return pages;
}
@Override
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
String[] pageUrls = trimmedHtml.split("],\\[");
for (int i = 0; i < pageUrls.length; i++) {
String[] urlParts = pageUrls[i].split(","); // auto/06/35,http://e4.adultmanga.me/,/55/01.png
String page = urlParts[1] + urlParts[0] + urlParts[2];
pages.get(i).setImageUrl(page);
}
return (List<Page>) pages;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}

View File

@ -0,0 +1,225 @@
package eu.kanade.tachiyomi.data.source.online.russian;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
public class Readmanga extends Source {
public static final String NAME = "Readmanga";
public static final String BASE_URL = "http://readmanga.me";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/list?sortType=rate";
public static final String SEARCH_URL = BASE_URL + "/search?q=%s";
public Readmanga(Context context) {
super(context);
}
@Override
public Language getLang() {
return LanguageKt.getRU();
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
protected String getInitialPopularMangasUrl() {
return POPULAR_MANGAS_URL;
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query));
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div.desc")) {
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
mangaList.add(manga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = currentHtmlBlock.getElementsByTag("h3").select("a").first();
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
String path = Parser.href(parsedHtml, "a:contains(→)");
return path != null ? BASE_URL + path : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element infoElement = parsedDocument.select("div.leftContent").first();
Manga manga = Manga.create(mangaUrl);
manga.title = Parser.text(infoElement, "span.eng-name");
manga.author = Parser.text(infoElement, "span.elem_author ");
manga.genre = Parser.allText(infoElement, "span.elem_genre ").replaceAll(" ,", ",");
manga.description = Parser.allText(infoElement, "div.manga-description");
if (Parser.text(infoElement, "h1.names:contains(Сингл)") != null) {
manga.status = Manga.COMPLETED;
} else {
manga.status = parseStatus(Parser.text(infoElement, "p:has(b:contains(Перевод:))"));
}
String thumbnail = Parser.element(infoElement, "img").attr("data-full");
if (thumbnail != null) {
manga.thumbnail_url = thumbnail;
}
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("продолжается")) {
return Manga.ONGOING;
}
if (status.contains("завершен")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("div.chapters-link tbody tr")) {
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(chapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = Parser.element(chapterElement, "a");
String date = Parser.text(chapterElement, "td:eq(1)");
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href") + "?mature=1");
chapter.name = urlElement.text().replaceAll(" новое", "");
}
if (date != null) {
try {
chapter.date_upload = new SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).getTime();
} catch (ParseException e) { /* Ignore */ }
}
return chapter;
}
// Without this extra chapters are in the wrong place in the list
@Override
public void parseChapterNumber(Chapter chapter) {
String url = chapter.url.replace("?mature=1", "");
String[] urlParts = url.replaceAll("/[\\w\\d]+/vol", "").split("/");
if (Float.parseFloat(urlParts[1]) < 1000f) {
urlParts[1] = "0" + urlParts[1];
}
if (Float.parseFloat(urlParts[1]) < 100f) {
urlParts[1] = "0" + urlParts[1];
}
if (Float.parseFloat(urlParts[1]) < 10f) {
urlParts[1] = "0" + urlParts[1];
}
chapter.chapter_number = Float.parseFloat(urlParts[0] + "." + urlParts[1]);
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
ArrayList<String> pages = new ArrayList<>();
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
String[] pageUrls = trimmedHtml.split("],\\[");
for (int i = 0; i < pageUrls.length; i++) {
pages.add("");
}
return pages;
}
@Override
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
String[] pageUrls = trimmedHtml.split("],\\[");
for (int i = 0; i < pageUrls.length; i++) {
String[] urlParts = pageUrls[i].split(","); // auto/12/56,http://e7.postfact.ru/,/51/01.jpg_res.jpg
String page = urlParts[1] + urlParts[0] + urlParts[2];
pages.get(i).setImageUrl(page);
}
return (List<Page>) pages;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}

View File

@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.data.sync;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import timber.log.Timber;
public class LibraryUpdateAlarm extends BroadcastReceiver {
public static final String LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY";
public static void startAlarm(Context context) {
startAlarm(context, PreferencesHelper.getLibraryUpdateInterval(context));
}
public static void startAlarm(Context context, int intervalInHours) {
stopAlarm(context);
if (intervalInHours == 0)
return;
int intervalInMillis = intervalInHours * 60 * 60 * 1000;
long nextRun = SystemClock.elapsedRealtime() + intervalInMillis;
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = getPendingIntent(context);
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
nextRun, intervalInMillis, pendingIntent);
Timber.i("Alarm set. Library will update on " + nextRun);
}
public static void stopAlarm(Context context) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = getPendingIntent(context);
alarmManager.cancel(pendingIntent);
}
private static PendingIntent getPendingIntent(Context context) {
Intent intent = new Intent(context, LibraryUpdateAlarm.class);
intent.setAction(LIBRARY_UPDATE_ACTION);
return PendingIntent.getBroadcast(context, 0, intent, 0);
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null)
return;
if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
startAlarm(context);
} else if (intent.getAction().equals(LIBRARY_UPDATE_ACTION)) {
LibraryUpdateService.start(context);
}
}
}

View File

@ -1,258 +0,0 @@
package eu.kanade.tachiyomi.data.sync;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.util.Pair;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Inject;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.BuildConfig;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.ui.main.MainActivity;
import eu.kanade.tachiyomi.util.AndroidComponentUtil;
import eu.kanade.tachiyomi.util.NetworkUtil;
import rx.Observable;
import rx.Subscription;
import rx.schedulers.Schedulers;
import timber.log.Timber;
public class LibraryUpdateService extends Service {
@Inject DatabaseHelper db;
@Inject SourceManager sourceManager;
@Inject PreferencesHelper preferences;
private PowerManager.WakeLock wakeLock;
private Subscription subscription;
public static final int UPDATE_NOTIFICATION_ID = 1;
public static void start(Context context) {
if (!isRunning(context)) {
context.startService(getStartIntent(context));
}
}
private static Intent getStartIntent(Context context) {
return new Intent(context, LibraryUpdateService.class);
}
private static boolean isRunning(Context context) {
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService.class);
}
@Override
public void onCreate() {
super.onCreate();
App.get(this).getComponent().inject(this);
createAndAcquireWakeLock();
}
@Override
public void onDestroy() {
if (subscription != null)
subscription.unsubscribe();
// Reset the alarm
LibraryUpdateAlarm.startAlarm(this);
destroyWakeLock();
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, final int startId) {
Timber.i("Starting sync...");
if (!NetworkUtil.isNetworkConnected(this)) {
Timber.i("Sync canceled, connection not available");
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable.class, true);
stopSelf(startId);
return START_NOT_STICKY;
}
subscription = Observable.fromCallable(() -> db.getFavoriteMangas().executeAsBlocking())
.subscribeOn(Schedulers.io())
.flatMap(this::updateLibrary)
.subscribe(next -> {},
error -> {
showNotification(getString(R.string.notification_update_error), "");
stopSelf(startId);
}, () -> {
Timber.i("Library updated");
stopSelf(startId);
});
return START_STICKY;
}
private Observable<MangaUpdate> updateLibrary(List<Manga> allLibraryMangas) {
final AtomicInteger count = new AtomicInteger(0);
final List<MangaUpdate> updates = new ArrayList<>();
final List<Manga> failedUpdates = new ArrayList<>();
final List<Manga> mangas = !preferences.updateOnlyNonCompleted() ? allLibraryMangas :
Observable.from(allLibraryMangas)
.filter(manga -> manga.status != Manga.COMPLETED)
.toList().toBlocking().single();
return Observable.from(mangas)
.doOnNext(manga -> showProgressNotification(
getString(R.string.notification_update_progress,
count.incrementAndGet(), mangas.size()), manga.title))
.concatMap(manga -> updateManga(manga)
.onErrorReturn(error -> {
failedUpdates.add(manga);
return Pair.create(0, 0);
})
// Filter out mangas without new chapters
.filter(pair -> pair.first > 0)
.map(pair -> new MangaUpdate(manga, pair.first)))
.doOnNext(updates::add)
.doOnCompleted(() -> {
if (updates.isEmpty()) {
cancelNotification();
} else {
showResultNotification(getString(R.string.notification_update_completed),
getUpdatedMangasResult(updates, failedUpdates));
}
});
}
private Observable<Pair<Integer, Integer>> updateManga(Manga manga) {
return sourceManager.get(manga.source)
.pullChaptersFromNetwork(manga.url)
.flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters));
}
private String getUpdatedMangasResult(List<MangaUpdate> updates, List<Manga> failedUpdates) {
final StringBuilder result = new StringBuilder();
if (updates.isEmpty()) {
result.append(getString(R.string.notification_no_new_chapters)).append("\n");
} else {
result.append(getString(R.string.notification_new_chapters));
for (MangaUpdate update : updates) {
result.append("\n").append(update.manga.title);
}
}
if (!failedUpdates.isEmpty()) {
result.append("\n");
result.append(getString(R.string.notification_manga_update_failed));
for (Manga manga : failedUpdates) {
result.append("\n").append(manga.title);
}
}
return result.toString();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void createAndAcquireWakeLock() {
wakeLock = ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock");
wakeLock.acquire();
}
private void destroyWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
}
private void showNotification(String title, String body) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_action_refresh)
.setContentTitle(title)
.setContentText(body);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
}
private void showProgressNotification(String title, String body) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_action_refresh)
.setContentTitle(title)
.setContentText(body)
.setOngoing(true);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
}
private void showResultNotification(String title, String body) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_action_refresh)
.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle().bigText(body))
.setContentIntent(getNotificationIntent())
.setAutoCancel(true);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
}
private void cancelNotification() {
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(UPDATE_NOTIFICATION_ID);
}
private PendingIntent getNotificationIntent() {
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
public static class SyncOnConnectionAvailable extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (NetworkUtil.isNetworkConnected(context)) {
if (BuildConfig.DEBUG) {
Timber.i("Connection is now available, triggering sync...");
}
AndroidComponentUtil.toggleComponent(context, this.getClass(), false);
context.startService(getStartIntent(context));
}
}
}
private static class MangaUpdate {
public Manga manga;
public int newChapters;
public MangaUpdate(Manga manga, int newChapters) {
this.manga = manga;
this.newChapters = newChapters;
}
}
}

View File

@ -1,79 +0,0 @@
package eu.kanade.tachiyomi.data.sync;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import javax.inject.Inject;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.MangaSync;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
public class UpdateMangaSyncService extends Service {
@Inject MangaSyncManager syncManager;
@Inject DatabaseHelper db;
private CompositeSubscription subscriptions;
private static final String EXTRA_MANGASYNC = "extra_mangasync";
public static void start(Context context, MangaSync mangaSync) {
Intent intent = new Intent(context, UpdateMangaSyncService.class);
intent.putExtra(EXTRA_MANGASYNC, mangaSync);
context.startService(intent);
}
@Override
public void onCreate() {
super.onCreate();
App.get(this).getComponent().inject(this);
subscriptions = new CompositeSubscription();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MangaSync mangaSync = (MangaSync) intent.getSerializableExtra(EXTRA_MANGASYNC);
updateLastChapterRead(mangaSync, startId);
return START_STICKY;
}
@Override
public void onDestroy() {
subscriptions.unsubscribe();
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void updateLastChapterRead(MangaSync mangaSync, int startId) {
MangaSyncService sync = syncManager.getSyncService(mangaSync.sync_id);
subscriptions.add(Observable.defer(() -> sync.update(mangaSync))
.flatMap(response -> {
if (response.isSuccessful()) {
return db.insertMangaSync(mangaSync).asRxObservable();
}
return Observable.error(new Exception("Could not update MAL"));
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
stopSelf(startId);
}, error -> {
stopSelf(startId);
}));
}
}

View File

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.data.updater
import com.google.gson.annotations.SerializedName
/**
* Release object.
* Contains information about the latest release from Github.
*
* @param version version of latest release.
* @param changeLog log of latest release.
* @param assets assets of latest release.
*/
class GithubRelease(@SerializedName("tag_name") val version: String,
@SerializedName("body") val changeLog: String,
@SerializedName("assets") val assets: List<Assets>) {
/**
* Get download link of latest release from the assets.
* @return download link of latest release.
*/
val downloadLink: String
get() = assets[0].downloadLink
/**
* Assets class containing download url.
* @param downloadLink download url.
*/
inner class Assets(@SerializedName("browser_download_url") val downloadLink: String)
}

View File

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.data.updater
import retrofit2.Retrofit
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import rx.Observable
/**
* Used to connect with the Github API.
*/
interface GithubService {
companion object {
fun create(): GithubService {
val restAdapter = Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
return restAdapter.create(GithubService::class.java)
}
}
@GET("/repos/inorichi/tachiyomi/releases/latest")
fun getLatestVersion(): Observable<GithubRelease>
}

View File

@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.toast
import rx.Observable
class GithubUpdateChecker(private val context: Context) {
val service: GithubService = GithubService.create()
/**
* Returns observable containing release information
*/
fun checkForApplicationUpdate(): Observable<GithubRelease> {
context.toast(R.string.update_check_look_for_updates)
return service.getLatestVersion()
}
}

View File

@ -1,31 +0,0 @@
package eu.kanade.tachiyomi.data.updater;
import android.content.Context;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.rest.GithubService;
import eu.kanade.tachiyomi.data.rest.Release;
import eu.kanade.tachiyomi.data.rest.ServiceFactory;
import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Observable;
public class UpdateChecker {
private final Context context;
public UpdateChecker(Context context) {
this.context = context;
}
/**
* Returns observable containing release information
*
*/
public Observable<Release> checkForApplicationUpdate() {
ToastUtil.showShort(context, context.getString(R.string.update_check_look_for_updates));
//Create Github service to retrieve Github data
GithubService service = ServiceFactory.createRetrofitService(GithubService.class, GithubService.SERVICE_ENDPOINT);
return service.getLatestVersion();
}
}

View File

@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.event;
public class ChapterCountEvent {
private int count;
public ChapterCountEvent(int count) {
this.count = count;
}
public int getCount() {
return count;
}
}

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.event
import rx.Observable
import rx.subjects.BehaviorSubject
class ChapterCountEvent() {
private val subject = BehaviorSubject.create<Int>()
val observable: Observable<Int>
get() = subject
fun emit(count: Int) {
subject.onNext(count)
}
}

View File

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.event;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class DownloadChaptersEvent {
private Manga manga;
private List<Chapter> chapters;
public DownloadChaptersEvent(Manga manga, List<Chapter> chapters) {
this.manga = manga;
this.chapters = chapters;
}
public Manga getManga() {
return manga;
}
public List<Chapter> getChapters() {
return chapters;
}
}

View File

@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.event
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
class DownloadChaptersEvent(val manga: Manga, val chapters: List<Chapter>)

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.event
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
class LibraryMangaEvent(val mangas: Map<Int, List<Manga>>) {
fun getMangasForCategory(category: Category): List<Manga>? {
return mangas[category.id]
}
}

View File

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.event;
import android.support.annotation.Nullable;
import java.util.List;
import java.util.Map;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class LibraryMangasEvent {
private final Map<Integer, List<Manga>> mangas;
public LibraryMangasEvent(Map<Integer, List<Manga>> mangas) {
this.mangas = mangas;
}
public Map<Integer, List<Manga>> getMangas() {
return mangas;
}
@Nullable
public List<Manga> getMangasForCategory(Category category) {
return mangas.get(category.id);
}
}

View File

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.event;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class MangaEvent {
public final Manga manga;
public MangaEvent(Manga manga) {
this.manga = manga;
}
}

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.event
import eu.kanade.tachiyomi.data.database.models.Manga
class MangaEvent(val manga: Manga)

View File

@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.event;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.base.Source;
public class ReaderEvent {
private Source source;
private Manga manga;
private Chapter chapter;
public ReaderEvent(Source source, Manga manga, Chapter chapter) {
this.source = source;
this.manga = manga;
this.chapter = chapter;
}
public Source getSource() {
return source;
}
public Manga getManga() {
return manga;
}
public Chapter getChapter() {
return chapter;
}
}

View File

@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.event
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
class ReaderEvent(val manga: Manga, val chapter: Chapter)

View File

@ -1,68 +0,0 @@
package eu.kanade.tachiyomi.injection.component;
import android.app.Application;
import javax.inject.Singleton;
import dagger.Component;
import eu.kanade.tachiyomi.data.download.DownloadService;
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService;
import eu.kanade.tachiyomi.data.updater.UpdateDownloader;
import eu.kanade.tachiyomi.injection.module.AppModule;
import eu.kanade.tachiyomi.injection.module.DataModule;
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter;
import eu.kanade.tachiyomi.ui.download.DownloadPresenter;
import eu.kanade.tachiyomi.ui.library.LibraryPresenter;
import eu.kanade.tachiyomi.ui.library.category.CategoryPresenter;
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
import eu.kanade.tachiyomi.ui.manga.MangaPresenter;
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter;
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter;
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListPresenter;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter;
import eu.kanade.tachiyomi.ui.recent.RecentChaptersPresenter;
import eu.kanade.tachiyomi.ui.setting.SettingsAccountsFragment;
import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
@Singleton
@Component(
modules = {
AppModule.class,
DataModule.class
}
)
public interface AppComponent {
void inject(LibraryPresenter libraryPresenter);
void inject(MangaPresenter mangaPresenter);
void inject(CataloguePresenter cataloguePresenter);
void inject(MangaInfoPresenter mangaInfoPresenter);
void inject(ChaptersPresenter chaptersPresenter);
void inject(ReaderPresenter readerPresenter);
void inject(DownloadPresenter downloadPresenter);
void inject(MyAnimeListPresenter myAnimeListPresenter);
void inject(CategoryPresenter categoryPresenter);
void inject(RecentChaptersPresenter recentChaptersPresenter);
void inject(ReaderActivity readerActivity);
void inject(MangaActivity mangaActivity);
void inject(SettingsAccountsFragment settingsAccountsFragment);
void inject(SettingsActivity settingsActivity);
void inject(Source source);
void inject(MyAnimeList myAnimeList);
void inject(LibraryUpdateService libraryUpdateService);
void inject(DownloadService downloadService);
void inject(UpdateMangaSyncService updateMangaSyncService);
void inject(UpdateDownloader updateDownloader);
Application application();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,160 @@
package eu.kanade.tachiyomi.ui.backup
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.fragment_backup.*
import nucleus.factory.RequiresPresenter
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.internal.util.SubscriptionList
import rx.schedulers.Schedulers
import timber.log.Timber
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
/**
* Fragment to create and restore backups of the application's data.
* Uses R.layout.fragment_backup.
*/
@RequiresPresenter(BackupPresenter::class)
class BackupFragment : BaseRxFragment<BackupPresenter>() {
private var backupDialog: Dialog? = null
private var restoreDialog: Dialog? = null
private lateinit var subscriptions: SubscriptionList
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
return inflater.inflate(R.layout.fragment_backup, container, false)
}
override fun onViewCreated(view: View, savedState: Bundle?) {
baseActivity.requestPermissionsOnMarshmallow()
subscriptions = SubscriptionList()
backup_button.setOnClickListener {
val today = SimpleDateFormat("yyyy-MM-dd").format(Date())
val file = File(activity.externalCacheDir, "tachiyomi-$today.json")
presenter.createBackup(file)
backupDialog = MaterialDialog.Builder(activity)
.content(R.string.backup_please_wait)
.progress(true, 0)
.show()
}
restore_button.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/octet-stream"
val chooser = Intent.createChooser(intent, getString(R.string.file_select_backup))
startActivityForResult(chooser, REQUEST_BACKUP_OPEN)
}
}
override fun onDestroyView() {
subscriptions.unsubscribe()
super.onDestroyView()
}
/**
* Called from the presenter when the backup is completed.
*
* @param file the file where the backup is saved.
*/
fun onBackupCompleted(file: File) {
dismissBackupDialog()
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + file))
startActivity(Intent.createChooser(intent, ""))
}
/**
* Called from the presenter when the restore is completed.
*/
fun onRestoreCompleted() {
dismissRestoreDialog()
context.toast(R.string.backup_completed)
}
/**
* Called from the presenter when there's an error doing the backup.
* @param error the exception thrown.
*/
fun onBackupError(error: Throwable) {
dismissBackupDialog()
context.toast(error.message)
}
/**
* Called from the presenter when there's an error restoring the backup.
* @param error the exception thrown.
*/
fun onRestoreError(error: Throwable) {
dismissRestoreDialog()
context.toast(error.message)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (data != null && resultCode == Activity.RESULT_OK && requestCode == REQUEST_BACKUP_OPEN) {
restoreDialog = MaterialDialog.Builder(activity)
.content(R.string.restore_please_wait)
.progress(true, 0)
.show()
// When using cloud services, we have to open the input stream in a background thread.
Observable.fromCallable { context.contentResolver.openInputStream(data.data) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
presenter.restoreBackup(it)
}, {
context.toast(it.message)
Timber.e(it, it.message)
})
.apply { subscriptions.add(this) }
}
}
/**
* Dismisses the backup dialog.
*/
fun dismissBackupDialog() {
backupDialog?.let {
it.dismiss()
backupDialog = null
}
}
/**
* Dismisses the restore dialog.
*/
fun dismissRestoreDialog() {
restoreDialog?.let {
it.dismiss()
restoreDialog = null
}
}
companion object {
private val REQUEST_BACKUP_OPEN = 102
fun newInstance(): BackupFragment {
return BackupFragment()
}
}
}

View File

@ -0,0 +1,93 @@
package eu.kanade.tachiyomi.ui.backup
import android.os.Bundle
import eu.kanade.tachiyomi.data.backup.BackupManager
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import java.io.File
import java.io.InputStream
import javax.inject.Inject
/**
* Presenter of [BackupFragment].
*/
class BackupPresenter : BasePresenter<BackupFragment>() {
/**
* Database.
*/
@Inject lateinit var db: DatabaseHelper
/**
* Backup manager.
*/
private lateinit var backupManager: BackupManager
/**
* Subscription where the backup is restored.
*/
private var restoreSubscription: Subscription? = null
/**
* Subscription where the backup is created.
*/
private var backupSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
backupManager = BackupManager(db)
}
/**
* Creates a backup and saves it to a file.
*
* @param file the path where the file will be saved.
*/
fun createBackup(file: File) {
if (isUnsubscribed(backupSubscription)) {
backupSubscription = getBackupObservable(file)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, result -> view.onBackupCompleted(file) },
{ view, error -> view.onBackupError(error) })
}
}
/**
* Restores a backup from a stream.
*
* @param stream the input stream of the backup file.
*/
fun restoreBackup(stream: InputStream) {
if (isUnsubscribed(restoreSubscription)) {
restoreSubscription = getRestoreObservable(stream)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, result -> view.onRestoreCompleted() },
{ view, error -> view.onRestoreError(error) })
}
}
/**
* Returns the observable to save a backup.
*/
private fun getBackupObservable(file: File) = Observable.fromCallable {
backupManager.backupToFile(file)
true
}
/**
* Returns the observable to restore a backup.
*/
private fun getRestoreObservable(stream: InputStream) = Observable.fromCallable {
backupManager.restoreFromStream(stream)
true
}
}

View File

@ -1,70 +0,0 @@
package eu.kanade.tachiyomi.ui.base.activity;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import org.greenrobot.eventbus.EventBus;
import icepick.Icepick;
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
Icepick.restoreInstanceState(this, savedState);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
protected void setupToolbar(Toolbar toolbar) {
setSupportActionBar(toolbar);
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
public void setToolbarTitle(String title) {
if (getSupportActionBar() != null)
getSupportActionBar().setTitle(title);
}
public void setToolbarTitle(int titleResource) {
if (getSupportActionBar() != null)
getSupportActionBar().setTitle(getString(titleResource));
}
public void setToolbarSubtitle(String title) {
if (getSupportActionBar() != null)
getSupportActionBar().setSubtitle(title);
}
public void setToolbarSubtitle(int titleResource) {
if (getSupportActionBar() != null)
getSupportActionBar().setSubtitle(getString(titleResource));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
public void registerForEvents() {
EventBus.getDefault().register(this);
}
public void unregisterForEvents() {
EventBus.getDefault().unregister(this);
}
}

View File

@ -0,0 +1,77 @@
package eu.kanade.tachiyomi.ui.base.activity
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.support.design.widget.Snackbar
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.View
import android.widget.TextView
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R
open class BaseActivity : AppCompatActivity() {
protected fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (backNavigation) {
toolbar.setNavigationOnClickListener { onBackPressed() }
}
}
fun setAppTheme() {
when (app.appTheme) {
2 -> setTheme(R.style.Theme_Tachiyomi_Dark)
else -> setTheme(R.style.Theme_Tachiyomi)
}
}
fun setToolbarTitle(title: String) {
supportActionBar?.title = title
}
fun setToolbarTitle(titleResource: Int) {
supportActionBar?.title = getString(titleResource)
}
fun setToolbarSubtitle(title: String) {
supportActionBar?.subtitle = title
}
fun setToolbarSubtitle(titleResource: Int) {
supportActionBar?.subtitle = getString(titleResource)
}
/**
* Requests read and write permissions on Android M and higher.
*/
fun requestPermissionsOnMarshmallow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
1)
}
}
}
protected val app: App
get() = App.get(this)
inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit) {
val snack = Snackbar.make(this, message, length)
val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
textView.setTextColor(Color.WHITE)
snack.f()
snack.show()
}
}

View File

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

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