Compare commits

...

32 Commits

Author SHA1 Message Date
634356e72f Release v0.6.7 2018-01-09 20:42:44 +01:00
6d3cc16ab1 Include minor changes from extensions PR 2018-01-09 12:27:45 +01:00
6027671c09 Address #1154 (#1160)
* change add to library icon add toast

* adjusted toast messages
added toast to catalog long click

* adjusted strings
2018-01-08 14:08:48 +01:00
29d0cb4a15 fixed issue where some sources that use cloudflare use the Server: cloudflare as cloudflare-nginx is deprecated (#1152) 2018-01-08 11:03:37 +01:00
fe7001975a Fix padding in RecyclerViews (#1148) 2018-01-06 18:50:40 +01:00
ac88f1c146 Update README (#1142)
* Update README.md

* add pics to readme

* Update README.md

* change language from stable to just "app"

* Update README.md

* update with feedback

* change test to try

* add link to extentions repo
2018-01-05 22:54:41 +01:00
b5b86218c5 Mangachan advanced support (#1138)
* Mangachan catalogue. Add support for filtering

* MangaChan add support for status
2018-01-04 22:01:42 +01:00
bdcc6e52e6 Small new user improvements (#1143)
- Changed empty library string
- Added empty view for Categories
2018-01-01 14:57:20 +01:00
0eae817aa6 Update MangaChan.kt (#1128)
Remove useless ganres
2017-12-14 13:28:24 +01:00
8994b42760 Remove local broadcast receiver to prevent race conditions (#1123)
* Remove local broadcast receiver to prevent run exceptions.
Added option to set tile for extension update.
2017-12-11 20:01:28 +01:00
6a63ce992a [Mangafox] update mangafox URL for built-in source (#1119) 2017-12-09 13:29:30 +01:00
9ae6285eef Change discord invite link in settings (#1112)
* Change discord invite link in settings

* Change discord link is readme
2017-12-06 08:41:37 +01:00
8f9737f567 Update regexp for pages from Readmanga/Mintmanga (#1111) 2017-12-05 21:21:02 +01:00
f287d313c3 Release 0.6.6 2017-12-05 19:50:53 +01:00
e745836404 Restore tracking on backup (#1097) 2017-12-04 22:55:57 +01:00
08baf798aa Give view pager unique ids, avoiding subtle bugs 2017-12-04 22:22:35 +01:00
8bcb14c65d Add view caching to view holders 2017-12-03 17:00:32 +01:00
d94dc68830 Fix library not being updated 2017-12-03 12:59:51 +01:00
297fed6aef Repackage catalogue to match the UI 2017-12-03 12:58:38 +01:00
d690d6e0e3 Use synthetic view's new caching method 2017-12-03 01:03:15 +01:00
9ba8d88b07 Dependency updates 2017-12-02 20:59:35 +01:00
34a40b0131 Start downloader after a library update. It should help with some catalogue issues 2017-12-02 17:29:05 +01:00
182bf5f2bd Add install packages permission. Fixes #1104 2017-12-02 17:10:31 +01:00
04638535d8 Fix library options menu shown in chapters screen. Resolves #1096 2017-11-30 15:37:20 +01:00
d87c8428fe Release 0.6.5 2017-11-29 18:49:22 +01:00
166fb9a8e4 Resubscribe to library when a change of type enter occurs. Resolves #1093 2017-11-29 10:05:33 +01:00
28a21d0b8f Minor changes to download cache. Also keep the library view, as recreation is expensive 2017-11-28 23:58:37 +01:00
d1d1d60c30 Fix automatic backups (#1074)
* Fix automatic backups

* Small fixes

* small fixes
2017-11-28 22:55:50 +01:00
80fd49d60b FIx Batoto issues with logging in and loading lists/pages. (#1088) 2017-11-28 09:48:27 +01:00
34eb1331a3 Update build tools in travis 2017-11-28 01:12:45 +01:00
bff329a329 Implement a download cache 2017-11-28 00:32:51 +01:00
604929d002 Update support library and kotlin 2017-11-28 00:21:38 +01:00
138 changed files with 2806 additions and 2524 deletions

BIN
.github/readme-images/app-icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
.github/readme-images/screens.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 KiB

View File

@ -1,7 +1,7 @@
language: android language: android
android: android:
components: components:
- build-tools-26.0.2 - build-tools-27.0.1
- android-26 - android-26
- extra-android-m2repository - extra-android-m2repository
- extra-google-m2repository - extra-google-m2repository

View File

@ -12,7 +12,7 @@ if [ -z "$TRAVIS_TAG" ]; then
else else
./gradlew clean assembleStandardRelease ./gradlew clean assembleStandardRelease
TOOLS="${ANDROID_HOME}/build-tools/26.0.1" TOOLS="$(ls -d ${ANDROID_HOME}/build-tools/* | tail -1)"
export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk" export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk"
${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk ${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk

View File

@ -1,25 +1,38 @@
| Build | Download | F-Droid | Contribute | Contact | | Build | Download | F-Droid | Contribute | Contact |
|-------|----------|---------|------------|---------| |-------|----------|---------|------------|---------|
| [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) | [![fdroid release](https://img.shields.io/badge/stable-f--droid.org-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid dev](https://img.shields.io/badge/dev-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [![Translation status](http://weblate.j2ghz.com/widgets/tachiyomi/-/svg-badge.svg)](https://github.com/inorichi/tachiyomi/wiki/Translation) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4) | | [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) | [![fdroid release](https://img.shields.io/badge/stable-f--droid.org-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid dev](https://img.shields.io/badge/dev-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [![Translation status](http://weblate.j2ghz.com/widgets/tachiyomi/-/svg-badge.svg)](https://github.com/inorichi/tachiyomi/wiki/Translation) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/2dDQBv2) |
### **Contact us on [Discord](https://discord.gg/WrBkRk4)**
If you want to open an issue, please read [contributing guidelines](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md). Your issue may be closed otherwise.
***
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
Tachiyomi is a free and open source manga reader for Android. 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. ![screenshots of app](./.github/readme-images/screens.png)
# Features ## Features
* Online and offline reading Features include:
* Configurable reader with multiple viewers and settings * Online reading from sources like Batoto, KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
* MyAnimeList support * Local reading of downloaded manga
* Resume from the next unread chapter * Configurable reader with multiple viewers, reading directions and other settings
* Chapter filtering * MyAnimeList, AniList, and Kitsu support
* Schedule searching for updates
* Categories to organize your library * Categories to organize your library
* Light and dark themes
* Schedule updating your library for new chapters
* Create backups locally or to your cloud service of choice
## Download
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases) or from [f-droid](https://f-droid.org/packages/eu.kanade.tachiyomi/).
If you want to try new features before they get to the stable release, you can download the dev version [here](http://tachiyomi.kanade.eu/latest) (auto-updates not included), or add our [F-Droid repo](https://github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions).
## Issues, Feature Requests and Contributing
If you want to open an issue, please read [contributing guidelines](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md). Your issue may be closed otherwise.
## FAQ
[See our wiki.](https://github.com/inorichi/tachiyomi/wiki/FAQ)
You can also reach out to us on [Discord](https://discord.gg/WrBkRk4).
## License ## License

View File

@ -30,7 +30,7 @@ ext {
android { android {
compileSdkVersion 26 compileSdkVersion 26
buildToolsVersion "26.0.2" buildToolsVersion "27.0.1"
publishNonDefault true publishNonDefault true
defaultConfig { defaultConfig {
@ -38,8 +38,8 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 26 targetSdkVersion 26
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 27 versionCode 30
versionName "0.6.4" versionName "0.6.7"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -106,7 +106,7 @@ dependencies {
implementation 'com.github.inorichi:junrar-android:634c1f5' implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library // Android support library
final support_library_version = '26.1.0' final support_library_version = '27.0.2'
implementation "com.android.support:support-v4:$support_library_version" implementation "com.android.support:support-v4:$support_library_version"
implementation "com.android.support:appcompat-v7:$support_library_version" implementation "com.android.support:appcompat-v7:$support_library_version"
implementation "com.android.support:cardview-v7:$support_library_version" implementation "com.android.support:cardview-v7:$support_library_version"
@ -118,17 +118,17 @@ dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support:multidex:1.0.1' implementation 'com.android.support:multidex:1.0.2'
// ReactiveX // ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.3' implementation 'io.reactivex:rxjava:1.3.4'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
implementation 'com.github.pwittchen:reactivenetwork:0.7.0' implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
// Network client // Network client
implementation "com.squareup.okhttp3:okhttp:3.9.0" implementation "com.squareup.okhttp3:okhttp:3.9.1"
implementation 'com.squareup.okio:okio:1.13.0' implementation 'com.squareup.okio:okio:1.13.0'
// REST // REST
@ -149,14 +149,14 @@ dependencies {
// Disk // Disk
implementation 'com.jakewharton:disklrucache:2.0.2' implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.seven332:unifile:1.0.0' implementation 'com.github.inorichi:unifile:e9ee588'
// HTML parser // HTML parser
implementation 'org.jsoup:jsoup:1.10.2' implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling // Job scheduling
implementation 'com.evernote:android-job:1.2.0' implementation 'com.evernote:android-job:1.2.1'
implementation 'com.google.android.gms:play-services-gcm:11.6.0' implementation 'com.google.android.gms:play-services-gcm:11.6.2'
// Changelog // Changelog
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
@ -232,7 +232,7 @@ dependencies {
} }
buildscript { buildscript {
ext.kotlin_version = '1.1.51' ext.kotlin_version = '1.2.0'
repositories { repositories {
mavenCentral() mavenCentral()
} }
@ -250,3 +250,6 @@ kotlin {
coroutines 'enable' coroutines 'enable'
} }
} }
androidExtensions {
experimental = true
}

View File

@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<application <application
@ -85,7 +86,7 @@
android:exported="false" /> android:exported="false" />
<service <service
android:name=".data.updater.UpdateDownloaderService" android:name=".data.updater.UpdaterService"
android:exported="false" /> android:exported="false" />
<service <service

View File

@ -8,7 +8,7 @@ import com.evernote.android.job.JobManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.util.LocaleHelper import eu.kanade.tachiyomi.util.LocaleHelper
import org.acra.ACRA import org.acra.ACRA
import org.acra.annotation.ReportsCrashes import org.acra.annotation.ReportsCrashes
@ -28,11 +28,11 @@ open class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
Injekt = InjektScope(DefaultRegistrar()) Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
setupAcra() setupAcra()
setupJobManager() setupJobManager()
setupNotificationChannels() setupNotificationChannels()
@ -60,7 +60,7 @@ open class App : Application() {
JobManager.create(this).addJobCreator { tag -> JobManager.create(this).addJobCreator { tag ->
when (tag) { when (tag) {
LibraryUpdateJob.TAG -> LibraryUpdateJob() LibraryUpdateJob.TAG -> LibraryUpdateJob()
UpdateCheckerJob.TAG -> UpdateCheckerJob() UpdaterJob.TAG -> UpdaterJob()
BackupCreatorJob.TAG -> BackupCreatorJob() BackupCreatorJob.TAG -> BackupCreatorJob()
else -> null else -> null
} }

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob import eu.kanade.tachiyomi.data.updater.UpdaterJob
import java.io.File import java.io.File
object Migrations { object Migrations {
@ -25,7 +25,7 @@ object Migrations {
if (oldVersion < 14) { if (oldVersion < 14) {
// Restore jobs after upgrading to evernote's job scheduler. // Restore jobs after upgrading to evernote's job scheduler.
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) { if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
UpdateCheckerJob.setupTask() UpdaterJob.setupTask()
} }
LibraryUpdateJob.setupTask() LibraryUpdateJob.setupTask()
} }

View File

@ -46,7 +46,7 @@ class BackupCreateService : IntentService(NAME) {
* Make a backup from library * Make a backup from library
* *
* @param context context of application * @param context context of application
* @param path path of Uri * @param uri path of Uri
* @param flags determines what to backup * @param flags determines what to backup
* @param isJob backup called from job * @param isJob backup called from job
*/ */
@ -80,7 +80,7 @@ class BackupCreateService : IntentService(NAME) {
* @param uri path of Uri * @param uri path of Uri
* @param isJob backup called from job * @param isJob backup called from job
*/ */
fun createBackupFromApp(uri: Uri, flags: Int, isJob: Boolean) { private fun createBackupFromApp(uri: Uri, flags: Int, isJob: Boolean) {
// Create root object // Create root object
val root = JsonObject() val root = JsonObject()
@ -113,8 +113,9 @@ class BackupCreateService : IntentService(NAME) {
try { try {
// When BackupCreatorJob // When BackupCreatorJob
if (isJob) { if (isJob) {
// Get dir of file // Get dir of file and create
val dir = UniFile.fromUri(this, uri) var dir = UniFile.fromUri(this, uri)
dir = dir.createDirectory("automatic")
// Delete older backups // Delete older backups
val numberOfBackups = backupManager.numberOfBackups() val numberOfBackups = backupManager.numberOfBackups()

View File

@ -8,13 +8,12 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
class BackupCreatorJob : Job() { class BackupCreatorJob : Job() {
override fun onRunJob(params: Params): Result { override fun onRunJob(params: Params): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val uri = Uri.fromFile(File(preferences.backupsDirectory().getOrDefault())) val uri = Uri.parse(preferences.backupsDirectory().getOrDefault())
val flags = BackupCreateService.BACKUP_ALL val flags = BackupCreateService.BACKUP_ALL
BackupCreateService.makeBackup(context, uri, flags, true) BackupCreateService.makeBackup(context, uri, flags, true)
return Result.SUCCESS return Result.SUCCESS

View File

@ -23,6 +23,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.* import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource import eu.kanade.tachiyomi.util.syncChaptersWithSource
@ -41,6 +42,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/ */
internal val sourceManager: SourceManager by injectLazy() internal val sourceManager: SourceManager by injectLazy()
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
/** /**
* Version of parser * Version of parser
*/ */
@ -67,18 +73,16 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
parser = initParser() parser = initParser()
} }
private fun initParser(): Gson { private fun initParser(): Gson = when (version) {
return when (version) { 1 -> GsonBuilder().create()
1 -> GsonBuilder().create() 2 -> GsonBuilder()
2 -> GsonBuilder() .registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build()) .registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build()) .registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build()) .registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build()) .registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build()) .create()
.create() else -> throw Exception("Json version unknown")
else -> throw Exception("Json version unknown")
}
} }
/** /**
@ -300,23 +304,26 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val trackToUpdate = ArrayList<Track>() val trackToUpdate = ArrayList<Track>()
for (track in tracks) { for (track in tracks) {
var isInDatabase = false val service = trackManager.getService(track.sync_id)
for (dbTrack in dbTracks) { if (service != null && service.isLogged) {
if (track.sync_id == dbTrack.sync_id) { var isInDatabase = false
// The sync is already in the db, only update its fields for (dbTrack in dbTracks) {
if (track.remote_id != dbTrack.remote_id) { if (track.sync_id == dbTrack.sync_id) {
dbTrack.remote_id = track.remote_id // The sync is already in the db, only update its fields
if (track.remote_id != dbTrack.remote_id) {
dbTrack.remote_id = track.remote_id
}
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
} }
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
} }
} if (!isInDatabase) {
if (!isInDatabase) { // Insert new sync. Let the db assign the id
// Insert new sync. Let the db assign the id track.id = null
track.id = null trackToUpdate.add(track)
trackToUpdate.add(track) }
} }
} }
// Update database // Update database
@ -361,32 +368,29 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* *
* @return [Manga], null if not found * @return [Manga], null if not found
*/ */
internal fun getMangaFromDatabase(manga: Manga): Manga? { internal fun getMangaFromDatabase(manga: Manga): Manga? =
return databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
}
/** /**
* Returns list containing manga from library * Returns list containing manga from library
* *
* @return [Manga] from library * @return [Manga] from library
*/ */
internal fun getFavoriteManga(): List<Manga> { internal fun getFavoriteManga(): List<Manga> =
return databaseHelper.getFavoriteMangas().executeAsBlocking() databaseHelper.getFavoriteMangas().executeAsBlocking()
}
/** /**
* Inserts manga and returns id * Inserts manga and returns id
* *
* @return id of [Manga], null if not found * @return id of [Manga], null if not found
*/ */
internal fun insertManga(manga: Manga): Long? { internal fun insertManga(manga: Manga): Long? =
return databaseHelper.insertManga(manga).executeAsBlocking().insertedId() databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
}
/** /**
* Inserts list of chapters * Inserts list of chapters
*/ */
internal fun insertChapters(chapters: List<Chapter>) { private fun insertChapters(chapters: List<Chapter>) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking() databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
} }
@ -395,7 +399,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* *
* @return number of backups selected by user * @return number of backups selected by user
*/ */
fun numberOfBackups(): Int { fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
return preferences.numberOfBackups().getOrDefault()
}
} }

View File

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
import eu.kanade.tachiyomi.data.backup.models.DHistory import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.* import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chop import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.isServiceRunning import eu.kanade.tachiyomi.util.isServiceRunning
@ -49,9 +50,8 @@ class BackupRestoreService : Service() {
* @param context the application context. * @param context the application context.
* @return true if the service is running, false otherwise. * @return true if the service is running, false otherwise.
*/ */
fun isRunning(context: Context): Boolean { private fun isRunning(context: Context): Boolean =
return context.isServiceRunning(BackupRestoreService::class.java) context.isServiceRunning(BackupRestoreService::class.java)
}
/** /**
* Starts a service to restore a backup from Json * Starts a service to restore a backup from Json
@ -113,7 +113,13 @@ class BackupRestoreService : Service() {
*/ */
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
lateinit var executor: ExecutorService /**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
private lateinit var executor: ExecutorService
/** /**
* Method called when the service is created. It injects dependencies and acquire the wake lock. * Method called when the service is created. It injects dependencies and acquire the wake lock.
@ -142,9 +148,7 @@ class BackupRestoreService : Service() {
/** /**
* This method needs to be implemented, but it's not used/needed. * This method needs to be implemented, but it's not used/needed.
*/ */
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? = null
return null
}
/** /**
* Method called when the service receives an intent. * Method called when the service receives an intent.
@ -164,7 +168,7 @@ class BackupRestoreService : Service() {
subscription = Observable.using( subscription = Observable.using(
{ db.lowLevel().beginTransaction() }, { db.lowLevel().beginTransaction() },
{ getRestoreObservable(uri).doOnNext{ db.lowLevel().setTransactionSuccessful() } }, { getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
{ executor.execute { db.lowLevel().endTransaction() } }) { executor.execute { db.lowLevel().endTransaction() } })
.doAfterTerminate { stopSelf(startId) } .doAfterTerminate { stopSelf(startId) }
.subscribeOn(Schedulers.from(executor)) .subscribeOn(Schedulers.from(executor))
@ -294,14 +298,14 @@ class BackupRestoreService : Service() {
val source = backupManager.sourceManager.get(manga.source) ?: return null val source = backupManager.sourceManager.get(manga.source) ?: return null
val dbManga = backupManager.getMangaFromDatabase(manga) val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) { return if (dbManga == null) {
// Manga not in database // Manga not in database
return mangaFetchObservable(source, manga, chapters, categories, history, tracks) mangaFetchObservable(source, manga, chapters, categories, history, tracks)
} else { // Manga in database } else { // Manga in database
// Copy information from manga already in database // Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga) backupManager.restoreMangaNoFetch(manga, dbManga)
// Fetch rest of manga information // Fetch rest of manga information
return mangaNoFetchObservable(source, manga, chapters, categories, history, tracks) mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
} }
} }
@ -327,14 +331,12 @@ class BackupRestoreService : Service() {
.map { manga } .map { manga }
} }
.doOnNext { .doOnNext {
// Restore categories restoreExtraForManga(it, categories, history, tracks)
backupManager.restoreCategoriesForManga(it, categories) }
.flatMap {
// Restore history trackingFetchObservable(it, tracks)
backupManager.restoreHistoryForManga(history) // Convert to the manga that contains new chapters.
.map { manga }
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
} }
.doOnCompleted { .doOnCompleted {
restoreProgress += 1 restoreProgress += 1
@ -356,14 +358,12 @@ class BackupRestoreService : Service() {
} }
} }
.doOnNext { .doOnNext {
// Restore categories restoreExtraForManga(it, categories, history, tracks)
backupManager.restoreCategoriesForManga(it, categories) }
.flatMap { manga ->
// Restore history trackingFetchObservable(manga, tracks)
backupManager.restoreHistoryForManga(history) // Convert to the manga that contains new chapters.
.map { manga }
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
} }
.doOnCompleted { .doOnCompleted {
restoreProgress += 1 restoreProgress += 1
@ -371,6 +371,17 @@ class BackupRestoreService : Service() {
} }
} }
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
// Restore categories
backupManager.restoreCategoriesForManga(manga, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(manga, tracks)
}
/** /**
* [Observable] that fetches chapter information * [Observable] that fetches chapter information
* *
@ -383,10 +394,33 @@ class BackupRestoreService : Service() {
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}") errors.add(Date() to "${manga.title} - ${it.message}")
Pair(emptyList<Chapter>(), emptyList<Chapter>()) Pair(emptyList(), emptyList())
} }
} }
/**
* [Observable] that refreshes tracking information
* @param manga manga that needs updating.
* @param tracks list containing tracks from restore file.
* @return [Observable] that contains updated track item
*/
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks)
.concatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
track
}
} else {
errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
Observable.empty()
}
}
}
/** /**
* Called to update dialog in [BackupConst] * Called to update dialog in [BackupConst]

View File

@ -0,0 +1,252 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
/**
* Cache where we dump the downloads directory from the filesystem. This class is needed because
* directory checking is expensive and it slowdowns the app. The cache is invalidated by the time
* defined in [renewInterval] as we don't have any control over the filesystem and the user can
* delete the folders at any time without the app noticing.
*
* @param context the application context.
* @param provider the downloads directories provider.
* @param sourceManager the source manager.
* @param preferences the preferences of the app.
*/
class DownloadCache(private val context: Context,
private val provider: DownloadProvider,
private val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()) {
/**
* The interval after which this cache should be invalidated. 1 hour shouldn't cause major
* issues, as the cache is only used for UI feedback.
*/
private val renewInterval = TimeUnit.HOURS.toMillis(1)
/**
* The last time the cache was refreshed.
*/
private var lastRenew = 0L
/**
* The root directory for downloads.
*/
private var rootDir = RootDirectory(getDirectoryFromPreference())
init {
preferences.downloadsDirectory().asObservable()
.skip(1)
.subscribe {
lastRenew = 0L // invalidate cache
rootDir = RootDirectory(getDirectoryFromPreference())
}
}
/**
* Returns the downloads directory from the user's preferences.
*/
private fun getDirectoryFromPreference(): UniFile {
val dir = preferences.downloadsDirectory().getOrDefault()
return UniFile.fromUri(context, Uri.parse(dir))
}
/**
* Returns true if the chapter is downloaded.
*
* @param chapter the chapter to check.
* @param manga the manga of the chapter.
* @param skipCache whether to skip the directory cache and check in the filesystem.
*/
fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean): Boolean {
if (skipCache) {
val source = sourceManager.get(manga.source) ?: return false
return provider.findChapterDir(chapter, manga, source) != null
}
checkRenew()
val sourceDir = rootDir.files[manga.source]
if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) {
return provider.getChapterDirName(chapter) in mangaDir.files
}
}
return false
}
/**
* Returns the amount of downloaded chapters for a manga.
*
* @param manga the manga to check.
*/
fun getDownloadCount(manga: Manga): Int {
checkRenew()
val sourceDir = rootDir.files[manga.source]
if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) {
return mangaDir.files.size
}
}
return 0
}
/**
* Checks if the cache needs a renewal and performs it if needed.
*/
@Synchronized
private fun checkRenew() {
if (lastRenew + renewInterval < System.currentTimeMillis()) {
renew()
lastRenew = System.currentTimeMillis()
}
}
/**
* Renews the downloads cache.
*/
private fun renew() {
val onlineSources = sourceManager.getOnlineSources()
val sourceDirs = rootDir.dir.listFiles()
.orEmpty()
.associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
}
rootDir.files = sourceDirs
sourceDirs.values.forEach { sourceDir ->
val mangaDirs = sourceDir.dir.listFiles()
.orEmpty()
.associateNotNullKeys { it.name to MangaDirectory(it) }
sourceDir.files = mangaDirs
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles()
.orEmpty()
.mapNotNull { it.name }
.toHashSet()
mangaDir.files = chapterDirs
}
}
}
/**
* Adds a chapter that has just been download to this cache.
*
* @param chapterDirName the downloaded chapter's directory name.
* @param mangaUniFile the directory of the manga.
* @param manga the manga of the chapter.
*/
@Synchronized
fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
// Retrieve the cached source directory or cache a new one
var sourceDir = rootDir.files[manga.source]
if (sourceDir == null) {
val source = sourceManager.get(manga.source) ?: return
val sourceUniFile = provider.findSourceDir(source) ?: return
sourceDir = SourceDirectory(sourceUniFile)
rootDir.files += manga.source to sourceDir
}
// Retrieve the cached manga directory or cache a new one
val mangaDirName = provider.getMangaDirName(manga)
var mangaDir = sourceDir.files[mangaDirName]
if (mangaDir == null) {
mangaDir = MangaDirectory(mangaUniFile)
sourceDir.files += mangaDirName to mangaDir
}
// Save the chapter directory
mangaDir.files += chapterDirName
}
/**
* Removes a chapter that has been deleted from this cache.
*
* @param chapter the chapter to remove.
* @param manga the manga of the chapter.
*/
@Synchronized
fun removeChapter(chapter: Chapter, manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
val chapterDirName = provider.getChapterDirName(chapter)
if (chapterDirName in mangaDir.files) {
mangaDir.files -= chapterDirName
}
}
/**
* Removes a manga that has been deleted from this cache.
*
* @param manga the manga to remove.
*/
@Synchronized
fun removeManga(manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return
val mangaDirName = provider.getMangaDirName(manga)
if (mangaDirName in sourceDir.files) {
sourceDir.files -= mangaDirName
}
}
/**
* Class to store the files under the root downloads directory.
*/
private class RootDirectory(val dir: UniFile,
var files: Map<Long, SourceDirectory> = hashMapOf())
/**
* Class to store the files under a source directory.
*/
private class SourceDirectory(val dir: UniFile,
var files: Map<String, MangaDirectory> = hashMapOf())
/**
* Class to store the files under a manga directory.
*/
private class MangaDirectory(val dir: UniFile,
var files: Set<String> = hashSetOf())
/**
* Returns a new map containing only the key entries of [transform] that are not null.
*/
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> {
val destination = LinkedHashMap<R, V>()
forEach { element -> transform(element)?.let { destination.put(it, element.value) } }
return destination
}
/**
* Returns a map from a list containing only the key entries of [transform] that are not null.
*/
private inline fun <T, K, V> Array<T>.associateNotNullKeys(transform: (T) -> Pair<K?, V>): Map<K, V> {
val destination = LinkedHashMap<K, V>()
for (element in this) {
val (key, value) = transform(element)
if (key != null) {
destination.put(key, value)
}
}
return destination
}
}

View File

@ -24,10 +24,15 @@ class DownloadManager(context: Context) {
*/ */
private val provider = DownloadProvider(context) private val provider = DownloadProvider(context)
/**
* Cache of downloaded chapters.
*/
private val cache = DownloadCache(context, provider)
/** /**
* Downloader whose only task is to download chapters. * Downloader whose only task is to download chapters.
*/ */
private val downloader = Downloader(context, provider) private val downloader = Downloader(context, provider, cache)
/** /**
* Downloads queue, where the pending chapters are stored. * Downloads queue, where the pending chapters are stored.
@ -80,9 +85,10 @@ class DownloadManager(context: Context) {
* *
* @param manga the manga of the chapters. * @param manga the manga of the chapters.
* @param chapters the list of chapters to enqueue. * @param chapters the list of chapters to enqueue.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/ */
fun downloadChapters(manga: Manga, chapters: List<Chapter>) { fun downloadChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean = true) {
downloader.queueChapters(manga, chapters) downloader.queueChapters(manga, chapters, autoStart)
} }
/** /**
@ -94,7 +100,7 @@ class DownloadManager(context: Context) {
* @return an observable containing the list of pages from the chapter. * @return an observable containing the list of pages from the chapter.
*/ */
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): Observable<List<Page>> { fun buildPageList(source: Source, manga: Manga, chapter: Chapter): Observable<List<Page>> {
return buildPageList(provider.findChapterDir(source, manga, chapter)) return buildPageList(provider.findChapterDir(chapter, manga, source))
} }
/** /**
@ -120,61 +126,45 @@ class DownloadManager(context: Context) {
} }
/** /**
* Returns the directory name for a manga. * Returns true if the chapter is downloaded.
* *
* @param manga the manga to query. * @param chapter the chapter to check.
*/
fun getMangaDirName(manga: Manga): String {
return provider.getMangaDirName(manga)
}
/**
* Returns the directory name for the given chapter.
*
* @param chapter the chapter to query.
*/
fun getChapterDirName(chapter: Chapter): String {
return provider.getChapterDirName(chapter)
}
/**
* Returns the download directory for a source if it exists.
*
* @param source the source to query.
*/
fun findSourceDir(source: Source): UniFile? {
return provider.findSourceDir(source)
}
/**
* Returns the directory for the given manga, if it exists.
*
* @param source the source of the manga.
* @param manga the manga to query.
*/
fun findMangaDir(source: Source, manga: Manga): UniFile? {
return provider.findMangaDir(source, manga)
}
/**
* Returns the directory for the given chapter, if it exists.
*
* @param source the source of the chapter.
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
* @param chapter the chapter to query. * @param skipCache whether to skip the directory cache and check in the filesystem.
*/ */
fun findChapterDir(source: Source, manga: Manga, chapter: Chapter): UniFile? { fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean = false): Boolean {
return provider.findChapterDir(source, manga, chapter) return cache.isChapterDownloaded(chapter, manga, skipCache)
}
/**
* Returns the amount of downloaded chapters for a manga.
*
* @param manga the manga to check.
*/
fun getDownloadCount(manga: Manga): Int {
return cache.getDownloadCount(manga)
} }
/** /**
* Deletes the directory of a downloaded chapter. * Deletes the directory of a downloaded chapter.
* *
* @param source the source of the chapter.
* @param manga the manga of the chapter.
* @param chapter the chapter to delete. * @param chapter the chapter to delete.
* @param manga the manga of the chapter.
* @param source the source of the chapter.
*/ */
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) { fun deleteChapter(chapter: Chapter, manga: Manga, source: Source) {
provider.findChapterDir(source, manga, chapter)?.delete() provider.findChapterDir(chapter, manga, source)?.delete()
cache.removeChapter(chapter, manga)
}
/**
* Deletes the directory of a downloaded manga.
*
* @param manga the manga to delete.
* @param source the source of the manga.
*/
fun deleteManga(manga: Manga, source: Source) {
provider.findMangaDir(manga, source)?.delete()
cache.removeManga(manga)
} }
} }

View File

@ -40,10 +40,10 @@ class DownloadProvider(private val context: Context) {
/** /**
* Returns the download directory for a manga. For internal use only. * Returns the download directory for a manga. For internal use only.
* *
* @param source the source of the manga.
* @param manga the manga to query. * @param manga the manga to query.
* @param source the source of the manga.
*/ */
internal fun getMangaDir(source: Source, manga: Manga): UniFile { internal fun getMangaDir(manga: Manga, source: Source): UniFile {
return downloadsDir return downloadsDir
.createDirectory(getSourceDirName(source)) .createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga)) .createDirectory(getMangaDirName(manga))
@ -61,10 +61,10 @@ class DownloadProvider(private val context: Context) {
/** /**
* Returns the download directory for a manga if it exists. * Returns the download directory for a manga if it exists.
* *
* @param source the source of the manga.
* @param manga the manga to query. * @param manga the manga to query.
* @param source the source of the manga.
*/ */
fun findMangaDir(source: Source, manga: Manga): UniFile? { fun findMangaDir(manga: Manga, source: Source): UniFile? {
val sourceDir = findSourceDir(source) val sourceDir = findSourceDir(source)
return sourceDir?.findFile(getMangaDirName(manga)) return sourceDir?.findFile(getMangaDirName(manga))
} }
@ -72,12 +72,12 @@ class DownloadProvider(private val context: Context) {
/** /**
* Returns the download directory for a chapter if it exists. * Returns the download directory for a chapter if it exists.
* *
* @param source the source of the chapter.
* @param manga the manga of the chapter.
* @param chapter the chapter to query. * @param chapter the chapter to query.
* @param manga the manga of the chapter.
* @param source the source of the chapter.
*/ */
fun findChapterDir(source: Source, manga: Manga, chapter: Chapter): UniFile? { fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
val mangaDir = findMangaDir(source, manga) val mangaDir = findMangaDir(manga, source)
return mangaDir?.findFile(getChapterDirName(chapter)) return mangaDir?.findFile(getChapterDirName(chapter))
} }

View File

@ -37,8 +37,11 @@ import uy.kohesive.injekt.injectLazy
* *
* @param context the application context. * @param context the application context.
* @param provider the downloads directory provider. * @param provider the downloads directory provider.
* @param cache the downloads cache, used to add the downloads to the cache after their completion.
*/ */
class Downloader(private val context: Context, private val provider: DownloadProvider) { class Downloader(private val context: Context,
private val provider: DownloadProvider,
private val cache: DownloadCache) {
/** /**
* Store for persisting downloads across restarts. * Store for persisting downloads across restarts.
@ -216,13 +219,14 @@ class Downloader(private val context: Context, private val provider: DownloadPro
* *
* @param manga the manga of the chapters to download. * @param manga the manga of the chapters to download.
* @param chapters the list of chapters to download. * @param chapters the list of chapters to download.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/ */
fun queueChapters(manga: Manga, chapters: List<Chapter>) = launchUI { fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
// Called in background thread, the operation can be slow with SAF. // Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async { val chaptersWithoutDir = async {
val mangaDir = provider.findMangaDir(source, manga) val mangaDir = provider.findMangaDir(manga, source)
chapters chapters
// Avoid downloading chapters with the same name. // Avoid downloading chapters with the same name.
@ -258,7 +262,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
} }
// Start downloader if needed // Start downloader if needed
DownloadService.start(this@Downloader.context) if (autoStart) {
DownloadService.start(this@Downloader.context)
}
} }
} }
@ -269,7 +275,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
*/ */
private fun downloadChapter(download: Download): Observable<Download> { private fun downloadChapter(download: Download): Observable<Download> {
val chapterDirname = provider.getChapterDirName(download.chapter) val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.source, download.manga) val mangaDir = provider.getMangaDir(download.manga, download.source)
val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp") val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp")
val pageListObservable = if (download.pages == null) { val pageListObservable = if (download.pages == null) {
@ -305,7 +311,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
.toList() .toList()
.map { _ -> download } .map { _ -> download }
// Do after download completes // Do after download completes
.doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) } .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
// If the page list threw, it will resume here // If the page list threw, it will resume here
.onErrorReturn { error -> .onErrorReturn { error ->
download.status = Download.ERROR download.status = Download.ERROR
@ -411,10 +417,13 @@ class Downloader(private val context: Context, private val provider: DownloadPro
* Checks if the download was successful. * Checks if the download was successful.
* *
* @param download the download to check. * @param download the download to check.
* @param mangaDir the manga directory of the download.
* @param tmpDir the directory where the download is currently stored. * @param tmpDir the directory where the download is currently stored.
* @param dirname the real (non temporary) directory name of the download. * @param dirname the real (non temporary) directory name of the download.
*/ */
private fun ensureSuccessfulDownload(download: Download, tmpDir: UniFile, dirname: String) { private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile,
tmpDir: UniFile, dirname: String) {
// Ensure that the chapter folder has all the images. // Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
@ -427,6 +436,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
// Only rename the directory if it's downloaded. // Only rename the directory if it's downloaded.
if (download.status == Download.DOWNLOADED) { if (download.status == Download.DOWNLOADED) {
tmpDir.renameTo(dirname) tmpDir.renameTo(dirname)
cache.addChapter(dirname, mangaDir, download.manga)
} }
} }

View File

@ -333,7 +333,9 @@ class LibraryUpdateService(
val dbChapters = chapters.map { val dbChapters = chapters.map {
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!! mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
} }
downloadManager.downloadChapters(manga, dbChapters) // We don't want to start downloading while the library is updating, because websites
// may don't like it and they could ban the user.
downloadManager.downloadChapters(manga, dbChapters, false)
} }
/** /**

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.getUriCompat import eu.kanade.tachiyomi.util.getUriCompat
import java.io.File import java.io.File
@ -43,11 +44,10 @@ object NotificationHandler {
* Returns [PendingIntent] that prompts user with apk install intent * Returns [PendingIntent] that prompts user with apk install intent
* *
* @param context context * @param context context
* @param file file of apk that is installed * @param uri uri of apk that is installed
*/ */
fun installApkPendingActivity(context: Context, file: File): PendingIntent { fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent {
val intent = Intent(Intent.ACTION_VIEW).apply { val intent = Intent(Intent.ACTION_VIEW).apply {
val uri = file.getUriCompat(context)
setDataAndType(uri, "application/vnd.android.package-archive") setDataAndType(uri, "application/vnd.android.package-archive")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
} }

View File

@ -11,8 +11,8 @@ import com.google.gson.annotations.SerializedName
* @param assets assets of latest release. * @param assets assets of latest release.
*/ */
class GithubRelease(@SerializedName("tag_name") val version: String, class GithubRelease(@SerializedName("tag_name") val version: String,
@SerializedName("body") val changeLog: String, @SerializedName("body") val changeLog: String,
@SerializedName("assets") val assets: List<Assets>) { @SerializedName("assets") private val assets: List<Assets>) {
/** /**
* Get download link of latest release from the assets. * Get download link of latest release from the assets.

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.updater
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import rx.Observable import rx.Observable
class GithubUpdateChecker() { class GithubUpdateChecker {
private val service: GithubService = GithubService.create() private val service: GithubService = GithubService.create()

View File

@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.data.updater
sealed class GithubUpdateResult { sealed class GithubUpdateResult {
class NewUpdate(val release: GithubRelease): GithubUpdateResult() class NewUpdate(val release: GithubRelease): GithubUpdateResult()
class NoNewUpdate(): GithubUpdateResult() class NoNewUpdate : GithubUpdateResult()
} }

View File

@ -1,147 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager
import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
* Local [BroadcastReceiver] that runs on UI thread
* Notification calls from [UpdateDownloaderService] should be made from here.
*/
internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceiver() {
companion object {
private const val NAME = "UpdateDownloaderReceiver"
// Called to show initial notification.
internal const val NOTIFICATION_UPDATER_INITIAL = "$ID.$NAME.UPDATER_INITIAL"
// Called to show progress notification.
internal const val NOTIFICATION_UPDATER_PROGRESS = "$ID.$NAME.UPDATER_PROGRESS"
// Called to show install notification.
internal const val NOTIFICATION_UPDATER_INSTALL = "$ID.$NAME.UPDATER_INSTALL"
// Called to show error notification
internal const val NOTIFICATION_UPDATER_ERROR = "$ID.$NAME.UPDATER_ERROR"
// Value containing action of BroadcastReceiver
internal const val EXTRA_ACTION = "$ID.$NAME.ACTION"
// Value containing progress
internal const val EXTRA_PROGRESS = "$ID.$NAME.PROGRESS"
// Value containing apk path
internal const val EXTRA_APK_PATH = "$ID.$NAME.APK_PATH"
// Value containing apk url
internal const val EXTRA_APK_URL = "$ID.$NAME.APK_URL"
}
/**
* Notification shown to user
*/
private val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
override fun onReceive(context: Context, intent: Intent) {
when (intent.getStringExtra(EXTRA_ACTION)) {
NOTIFICATION_UPDATER_INITIAL -> basicNotification()
NOTIFICATION_UPDATER_PROGRESS -> updateProgress(intent.getIntExtra(EXTRA_PROGRESS, 0))
NOTIFICATION_UPDATER_INSTALL -> installNotification(intent.getStringExtra(EXTRA_APK_PATH))
NOTIFICATION_UPDATER_ERROR -> errorNotification(intent.getStringExtra(EXTRA_APK_URL))
}
}
/**
* Called to show basic notification
*/
private fun basicNotification() {
// Create notification
with(notification) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
setOngoing(true)
}
notification.show()
}
/**
* Called to show progress notification
*
* @param progress progress of download
*/
private fun updateProgress(progress: Int) {
with(notification) {
setProgress(100, progress, false)
setOnlyAlertOnce(true)
}
notification.show()
}
/**
* Called to show install notification
*
* @param path path of file
*/
private fun installNotification(path: String) {
// Prompt the user to install the new update.
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
addAction(R.drawable.ic_system_update_grey_24dp_img,
context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, File(path)))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show()
}
/**
* Called to show error notification
*
* @param url url of apk
*/
private fun errorNotification(url: String) {
// Prompt the user to retry the download.
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Retry action
addAction(R.drawable.ic_refresh_grey_24dp_img,
context.getString(R.string.action_retry),
UpdateDownloaderService.downloadApkPendingService(context, url))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show()
}
/**
* Shows a notification from this builder.
*
* @param id the id of the notification.
*/
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
context.notificationManager.notify(id, build())
}
}

View File

@ -1,67 +1,67 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import com.evernote.android.job.Job import com.evernote.android.job.Job
import com.evernote.android.job.JobManager import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest import com.evernote.android.job.JobRequest
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
class UpdateCheckerJob : Job() { class UpdaterJob : Job() {
override fun onRunJob(params: Params): Result { override fun onRunJob(params: Params): Result {
return GithubUpdateChecker() return GithubUpdateChecker()
.checkForUpdate() .checkForUpdate()
.map { result -> .map { result ->
if (result is GithubUpdateResult.NewUpdate) { if (result is GithubUpdateResult.NewUpdate) {
val url = result.release.downloadLink val url = result.release.downloadLink
val intent = Intent(context, UpdateDownloaderService::class.java).apply { val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url) putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
} }
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update { NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
setContentTitle(context.getString(R.string.app_name)) setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available)) setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done) setSmallIcon(android.R.drawable.stat_sys_download_done)
// Download action // Download action
addAction(android.R.drawable.stat_sys_download_done, addAction(android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download), context.getString(R.string.action_download),
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
} }
} }
Job.Result.SUCCESS Job.Result.SUCCESS
} }
.onErrorReturn { Job.Result.FAILURE } .onErrorReturn { Job.Result.FAILURE }
// Sadly, the task needs to be synchronous. // Sadly, the task needs to be synchronous.
.toBlocking() .toBlocking()
.single() .single()
} }
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block() block()
context.notificationManager.notify(Notifications.ID_UPDATER, build()) context.notificationManager.notify(Notifications.ID_UPDATER, build())
} }
companion object { companion object {
const val TAG = "UpdateChecker" const val TAG = "UpdateChecker"
fun setupTask() { fun setupTask() {
JobRequest.Builder(TAG) JobRequest.Builder(TAG)
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000) .setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true) .setRequirementsEnforced(true)
.setUpdateCurrent(true) .setUpdateCurrent(true)
.build() .build()
.schedule() .schedule()
} }
fun cancelTask() { fun cancelTask() {
JobManager.instance().cancelAllForTag(TAG) JobManager.instance().cancelAllForTag(TAG)
} }
} }
} }

View File

@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
import android.net.Uri
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager
/**
* DownloadNotifier is used to show notifications when downloading and update.
*
* @param context context of application.
*/
internal class UpdaterNotifier(private val context: Context) {
/**
* Builder to manage notifications.
*/
private val notification by lazy {
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
}
/**
* Call to show notification.
*
* @param id id of the notification channel.
*/
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
context.notificationManager.notify(id, build())
}
/**
* Call when apk download starts.
*
* @param title tile of notification.
*/
fun onDownloadStarted(title: String) {
with(notification) {
setContentTitle(title)
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
setOngoing(true)
}
notification.show()
}
/**
* Call when apk download progress changes.
*
* @param progress progress of download (xx%/100).
*/
fun onProgressChange(progress: Int) {
with(notification) {
setProgress(100, progress, false)
setOnlyAlertOnce(true)
}
notification.show()
}
/**
* Call when apk download is finished.
*
* @param uri path location of apk.
*/
fun onDownloadFinished(uri: Uri) {
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
addAction(R.drawable.ic_system_update_grey_24dp_img,
context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, uri))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show()
}
/**
* Call when apk download throws a error
*
* @param url web location of apk to download.
*/
fun onDownloadError(url: String) {
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Retry action
addAction(R.drawable.ic_refresh_grey_24dp_img,
context.getString(R.string.action_retry),
UpdaterService.downloadApkPendingService(context, url))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show(Notifications.ID_UPDATER)
}
}

View File

@ -2,52 +2,37 @@ package eu.kanade.tachiyomi.data.updater
import android.app.IntentService import android.app.IntentService
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.newCallWithProgress import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.util.registerLocalReceiver import eu.kanade.tachiyomi.util.getUriCompat
import eu.kanade.tachiyomi.util.saveTo import eu.kanade.tachiyomi.util.saveTo
import eu.kanade.tachiyomi.util.sendLocalBroadcastSync
import eu.kanade.tachiyomi.util.unregisterLocalReceiver
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) { class UpdaterService : IntentService(UpdaterService::class.java.name) {
/** /**
* Network helper * Network helper
*/ */
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
/** /**
* Local [BroadcastReceiver] that runs on UI thread * Notifier for the updater state and progress.
*/ */
private val updaterNotificationReceiver = UpdateDownloaderReceiver(this) private val notifier by lazy { UpdaterNotifier(this) }
override fun onCreate() {
super.onCreate()
// Register receiver
registerLocalReceiver(updaterNotificationReceiver, IntentFilter(INTENT_FILTER_NAME))
}
override fun onDestroy() {
// Unregister receiver
unregisterLocalReceiver(updaterNotificationReceiver)
super.onDestroy()
}
override fun onHandleIntent(intent: Intent?) { override fun onHandleIntent(intent: Intent?) {
if (intent == null) return if (intent == null) return
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
downloadApk(url) downloadApk(title, url)
} }
/** /**
@ -55,9 +40,9 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
* *
* @param url url location of file * @param url url location of file
*/ */
fun downloadApk(url: String) { private fun downloadApk(title: String, url: String) {
// Show notification download starting. // Show notification download starting.
sendInitialBroadcast() notifier.onDownloadStarted(title)
val progressListener = object : ProgressListener { val progressListener = object : ProgressListener {
@ -73,7 +58,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
if (progress > savedProgress && currentTime - 200 > lastTick) { if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress savedProgress = progress
lastTick = currentTime lastTick = currentTime
sendProgressBroadcast(progress) notifier.onProgressChange(progress)
} }
} }
} }
@ -91,80 +76,32 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
response.close() response.close()
throw Exception("Unsuccessful response") throw Exception("Unsuccessful response")
} }
sendInstallBroadcast(apkFile.absolutePath) notifier.onDownloadFinished(apkFile.getUriCompat(this))
} catch (error: Exception) { } catch (error: Exception) {
Timber.e(error) Timber.e(error)
sendErrorBroadcast(url) notifier.onDownloadError(url)
} }
} }
/**
* Show notification download starting.
*/
private fun sendInitialBroadcast() {
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INITIAL)
}
sendLocalBroadcastSync(intent)
}
/**
* Show notification progress changed
*
* @param progress progress of download
*/
private fun sendProgressBroadcast(progress: Int) {
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS)
putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress)
}
sendLocalBroadcastSync(intent)
}
/**
* Show install notification.
*
* @param path location of file
*/
private fun sendInstallBroadcast(path: String){
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INSTALL)
putExtra(UpdateDownloaderReceiver.EXTRA_APK_PATH, path)
}
sendLocalBroadcastSync(intent)
}
/**
* Show error notification.
*
* @param url url of file
*/
private fun sendErrorBroadcast(url: String){
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_ERROR)
putExtra(UpdateDownloaderReceiver.EXTRA_APK_URL, url)
}
sendLocalBroadcastSync(intent)
}
companion object { companion object {
/**
* Name of Local BroadCastReceiver.
*/
private val INTENT_FILTER_NAME = UpdateDownloaderService::class.java.name
/** /**
* Download url. * Download url.
*/ */
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdateDownloaderService.DOWNLOAD_URL" internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
/**
* Download title
*/
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
/** /**
* Downloads a new update and let the user install the new version from a notification. * Downloads a new update and let the user install the new version from a notification.
* @param context the application context. * @param context the application context.
* @param url the url to the new update. * @param url the url to the new update.
*/ */
fun downloadUpdate(context: Context, url: String) { fun downloadUpdate(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
val intent = Intent(context, UpdateDownloaderService::class.java).apply { val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url) putExtra(EXTRA_DOWNLOAD_URL, url)
} }
context.startService(intent) context.startService(intent)
@ -177,7 +114,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
* @return [PendingIntent] * @return [PendingIntent]
*/ */
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent { internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
val intent = Intent(context, UpdateDownloaderService::class.java).apply { val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_URL, url) putExtra(EXTRA_DOWNLOAD_URL, url)
} }
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

View File

@ -14,12 +14,14 @@ class CloudflareInterceptor : Interceptor {
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""") private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
@Synchronized @Synchronized
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request()) val response = chain.proceed(chain.request())
// Check if Cloudflare anti-bot is on // Check if Cloudflare anti-bot is on
if (response.code() == 503 && "cloudflare-nginx" == response.header("Server")) { if (response.code() == 503 && serverCheck.contains(response.header("Server"))) {
return chain.proceed(resolveChallenge(response)) return chain.proceed(resolveChallenge(response))
} }

View File

@ -18,16 +18,6 @@ class NetworkHelper(context: Context) {
.cache(Cache(cacheDir, cacheSize)) .cache(Cache(cacheDir, cacheSize))
.build() .build()
val forceCacheClient = client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=600")
.build()
}
.build()
val cloudflareClient = client.newBuilder() val cloudflareClient = client.newBuilder()
.addInterceptor(CloudflareInterceptor()) .addInterceptor(CloudflareInterceptor())
.build() .build()

View File

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.selectText import eu.kanade.tachiyomi.util.selectText
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -48,6 +49,8 @@ class Batoto : ParsedHttpSource(), LoginSource {
private val staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE) private val staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE)
override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.add("Cookie", "lang_option=English") .add("Cookie", "lang_option=English")

View File

@ -17,7 +17,7 @@ class Mangafox : ParsedHttpSource() {
override val name = "Mangafox" override val name = "Mangafox"
override val baseUrl = "http://mangafox.me" override val baseUrl = "http://mangafox.la"
override val lang = "en" override val lang = "en"

View File

@ -35,13 +35,59 @@ class Mangachan : ParsedHttpSource() {
val url = if (query.isNotEmpty()) { val url = if (query.isNotEmpty()) {
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum" "$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
} else { } else {
val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
if (filt.isNotEmpty()) { var genres = ""
var genres = "" var order = ""
filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' } var statusParam = true
"$baseUrl/tags/${genres.dropLast(1)}?offset=${20 * (pageNum - 1)}" var status = ""
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
when (filter) {
is GenreList -> {
filter.state.forEach { f ->
if (!f.isIgnored()) {
genres += (if (f.isExcluded()) "-" else "") + f.id + '+'
}
}
}
is OrderBy -> { if (filter.state!!.ascending && filter.state!!.index == 0) { statusParam = false } }
is Status -> status = arrayOf("", "all_done", "end", "ongoing", "new_ch")[filter.state]
}
}
if (genres.isNotEmpty()) {
for (filter in filters) {
when (filter) {
is OrderBy -> {
order = if (filter.state!!.ascending) {
arrayOf("", "&n=favasc", "&n=abcdesc", "&n=chasc")[filter.state!!.index]
} else {
arrayOf("&n=dateasc", "&n=favdesc", "&n=abcasc", "&n=chdesc")[filter.state!!.index]
}
}
}
}
if (statusParam) {
"$baseUrl/tags/${genres.dropLast(1)}$order?offset=${20 * (pageNum - 1)}&status=$status"
} else {
"$baseUrl/tags/$status/${genres.dropLast(1)}/$order?offset=${20 * (pageNum - 1)}"
}
} else { } else {
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum" for (filter in filters) {
when (filter) {
is OrderBy -> {
order = if (filter.state!!.ascending) {
arrayOf("manga/new", "manga/new&n=favasc", "manga/new&n=abcdesc", "manga/new&n=chasc")[filter.state!!.index]
} else {
arrayOf("manga/new&n=dateasc", "mostfavorites", "catalog", "sortch")[filter.state!!.index]
}
}
}
}
if (statusParam) {
"$baseUrl/$order?offset=${20 * (pageNum - 1)}&status=$status"
} else {
"$baseUrl/$order/$status?offset=${20 * (pageNum - 1)}"
}
} }
} }
return GET(url, headers) return GET(url, headers)
@ -160,18 +206,39 @@ class Mangachan : ParsedHttpSource() {
override fun imageUrlParse(document: Document) = "" override fun imageUrlParse(document: Document) = ""
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name) private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
private class OrderBy : Filter.Sort("Сортировка",
arrayOf("Дата", "Популярность", "Имя", "Главы"),
Filter.Sort.Selection(1, false))
override fun getFilterList() = FilterList(
Status(),
OrderBy(),
GenreList(getGenreList())
)
// private class StatusList(status: List<Status>) : Filter.Group<Status>("Статус", status)
// private class Status(name: String, val id: String) : Filter.CheckBox(name, false)
// private fun getStatusList() = listOf(
// Status("Перевод завершен", "/all_done"),
// Status("Выпуск завершен", "/end"),
// Status("Онгоинг", "/ongoing"),
// Status("Новые главы", "/new_ch")
// )
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) => /* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
* { const link=el.getAttribute('href');const id=link.substr(6,link.length); * { const link=el.getAttribute('href');const id=link.substr(6,link.length);
* return `Genre("${id.replace("_", " ")}")` }).join(',\n') * return `Genre("${id.replace("_", " ")}")` }).join(',\n')
* on http://mangachan.me/ * on http://mangachan.me/
*/ */
override fun getFilterList() = FilterList( private fun getGenreList() = listOf(
Genre("18 плюс"), Genre("18 плюс"),
Genre("bdsm"), Genre("bdsm"),
Genre("арт"), Genre("арт"),
Genre("биография"),
Genre("боевик"), Genre("боевик"),
Genre("боевые искусства"), Genre("боевые искусства"),
Genre("вампиры"), Genre("вампиры"),
@ -191,7 +258,6 @@ class Mangachan : ParsedHttpSource() {
Genre("кодомо"), Genre("кодомо"),
Genre("комедия"), Genre("комедия"),
Genre("литРПГ"), Genre("литРПГ"),
Genre("магия"),
Genre("махо-сёдзё"), Genre("махо-сёдзё"),
Genre("меха"), Genre("меха"),
Genre("мистика"), Genre("мистика"),
@ -213,6 +279,7 @@ class Mangachan : ParsedHttpSource() {
Genre("сёдзё-ай"), Genre("сёдзё-ай"),
Genre("сёнэн"), Genre("сёнэн"),
Genre("сёнэн-ай"), Genre("сёнэн-ай"),
Genre("темное фэнтези"),
Genre("тентакли"), Genre("тентакли"),
Genre("трагедия"), Genre("трагедия"),
Genre("триллер"), Genre("триллер"),
@ -226,4 +293,4 @@ class Mangachan : ParsedHttpSource() {
Genre("яой"), Genre("яой"),
Genre("ёнкома") Genre("ёнкома")
) )
} }

View File

@ -118,7 +118,7 @@ class Mintmanga : ParsedHttpSource() {
val endIndex = html.indexOf("], 0, false);", beginIndex) val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex) val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("'.+?','.+?',\".+?\"") val p = Pattern.compile("'.*?','.*?',\".*?\"")
val m = p.matcher(trimmedHtml) val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()

View File

@ -118,7 +118,7 @@ class Readmanga : ParsedHttpSource() {
val endIndex = html.indexOf("], 0, false);", beginIndex) val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex) val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("'.+?','.+?',\".+?\"") val p = Pattern.compile("'.*?','.*?',\".*?\"")
val m = p.matcher(trimmedHtml) val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()

View File

@ -6,21 +6,39 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RestoreViewOnCreateController import com.bluelinelabs.conductor.RestoreViewOnCreateController
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.*
abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle) { abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle),
LayoutContainer {
init {
addLifecycleListener(object : LifecycleListener() {
override fun postCreateView(controller: Controller, view: View) {
onViewCreated(view)
}
})
}
override val containerView: View?
get() = view
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
val view = inflateView(inflater, container) return inflateView(inflater, container)
onViewCreated(view, savedViewState) }
return view
override fun onDestroyView(view: View) {
super.onDestroyView(view)
clearFindViewByIdCache()
} }
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
open fun onViewCreated(view: View, savedViewState: Bundle?) { } open fun onViewCreated(view: View) { }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) { if (type.isEnter) {

View File

@ -5,6 +5,8 @@ import android.os.Build
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
fun Router.popControllerWithTag(tag: String): Boolean { fun Router.popControllerWithTag(tag: String): Boolean {
val controller = getControllerWithTag(tag) val controller = getControllerWithTag(tag)
@ -24,4 +26,10 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
} }
} }
} }
} }
fun Controller.withFadeTransaction(): RouterTransaction {
return RouterTransaction.with(this)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
}

View File

@ -30,7 +30,7 @@ abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) {
} }
@CallSuper @CallSuper
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
if (untilDestroySubscriptions.isUnsubscribed) { if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription() untilDestroySubscriptions = CompositeSubscription()
} }

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import kotlinx.android.extensions.LayoutContainer
abstract class BaseFlexibleViewHolder(view: View,
adapter: FlexibleAdapter<*>,
stickyHeader: Boolean = false) :
FlexibleViewHolder(view, adapter, stickyHeader), LayoutContainer {
override val containerView: View?
get() = itemView
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.extensions.LayoutContainer
abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view), LayoutContainer {
override val containerView: View?
get() = itemView
}

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.base.presenter package eu.kanade.tachiyomi.ui.base.presenter
import nucleus.presenter.RxPresenter import nucleus.presenter.RxPresenter
import nucleus.presenter.delivery.Delivery
import rx.Observable import rx.Observable
open class BasePresenter<V> : RxPresenter<V>() { open class BasePresenter<V> : RxPresenter<V>() {
@ -35,4 +36,29 @@ open class BasePresenter<V> : RxPresenter<V>() {
fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
= compose(deliverReplay<T>()).subscribe(split(onNext, onError)).apply { add(this) } = compose(deliverReplay<T>()).subscribe(split(onNext, onError)).apply { add(this) }
/**
* Subscribes an observable with [DeliverWithView] and adds it to the presenter's lifecycle
* subscription list.
*
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
fun <T> Observable<T>.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
= compose(DeliverWithView<V, T>(view())).subscribe(split(onNext, onError)).apply { add(this) }
/**
* A deliverable that only emits to the view if attached, otherwise the event is ignored.
*/
class DeliverWithView<View, T>(private val view: Observable<View>) : Observable.Transformer<T, Delivery<View, T>> {
override fun call(observable: Observable<T>): Observable<Delivery<View, T>> {
return observable
.materialize()
.filter { notification -> !notification.isOnCompleted }
.flatMap { notification ->
view.take(1).filter { it != null }.map { Delivery(it, notification) }
}
}
}
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
@ -8,9 +8,9 @@ import eu.kanade.tachiyomi.util.getResourceColor
/** /**
* Adapter that holds the catalogue cards. * Adapter that holds the catalogue cards.
* *
* @param controller instance of [CatalogueMainController]. * @param controller instance of [CatalogueController].
*/ */
class CatalogueMainAdapter(val controller: CatalogueMainController) : class CatalogueAdapter(val controller: CatalogueController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) { FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
@ -31,7 +31,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
/** /**
* Listener which should be called when user clicks browse. * Listener which should be called when user clicks browse.
* Note: Should only be handled by [CatalogueMainController] * Note: Should only be handled by [CatalogueController]
*/ */
interface OnBrowseClickListener { interface OnBrowseClickListener {
fun onBrowseClick(position: Int) fun onBrowseClick(position: Int)
@ -39,7 +39,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
/** /**
* Listener which should be called when user clicks latest. * Listener which should be called when user clicks latest.
* Note: Should only be handled by [CatalogueMainController] * Note: Should only be handled by [CatalogueController]
*/ */
interface OnLatestClickListener { interface OnLatestClickListener {
fun onLatestClick(position: Int) fun onLatestClick(position: Int)

View File

@ -1,523 +1,231 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.content.res.Configuration import android.support.v7.widget.LinearLayoutManager
import android.os.Bundle import android.support.v7.widget.SearchView
import android.support.design.widget.Snackbar import android.view.*
import android.support.v4.widget.DrawerLayout import com.bluelinelabs.conductor.ControllerChangeHandler
import android.support.v7.widget.* import com.bluelinelabs.conductor.ControllerChangeType
import android.view.* import com.bluelinelabs.conductor.RouterTransaction
import com.afollestad.materialdialogs.MaterialDialog import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.bluelinelabs.conductor.RouterTransaction import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import eu.davidea.flexibleadapter.FlexibleAdapter
import com.f2prateek.rx.preferences.Preference import eu.davidea.flexibleadapter.items.IFlexible
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import eu.kanade.tachiyomi.R
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import kotlinx.android.synthetic.main.catalogue_main_controller.*
import eu.kanade.tachiyomi.ui.manga.MangaController import uy.kohesive.injekt.Injekt
import eu.kanade.tachiyomi.util.* import uy.kohesive.injekt.api.get
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener /**
import kotlinx.android.synthetic.main.catalogue_controller.view.* * This controller shows and manages the different catalogues enabled by the user.
import kotlinx.android.synthetic.main.main_activity.* * This controller should only handle UI actions, IO actions should be done by [CataloguePresenter]
import rx.Observable * [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
import rx.Subscription * [CatalogueAdapter.OnBrowseClickListener] call function data on browse item click.
import rx.android.schedulers.AndroidSchedulers * [CatalogueAdapter.OnLatestClickListener] call function data on latest item click
import rx.subscriptions.Subscriptions */
import timber.log.Timber class CatalogueController : NucleusController<CataloguePresenter>(),
import uy.kohesive.injekt.injectLazy SourceLoginDialog.Listener,
import java.util.concurrent.TimeUnit FlexibleAdapter.OnItemClickListener,
CatalogueAdapter.OnBrowseClickListener,
/** CatalogueAdapter.OnLatestClickListener {
* Controller to manage the catalogues available in the app.
*/ /**
open class CatalogueController(bundle: Bundle) : * Application preferences.
NucleusController<CataloguePresenter>(bundle), */
SecondaryDrawerController, private val preferences: PreferencesHelper = Injekt.get()
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, /**
FlexibleAdapter.EndlessScrollListener, * Adapter containing sources.
ChangeMangaCategoriesDialog.Listener { */
private var adapter: CatalogueAdapter? = null
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id) /**
}) * Called when controller is initialized.
*/
/** init {
* Preferences helper. // Enable the option menu
*/ setHasOptionsMenu(true)
private val preferences: PreferencesHelper by injectLazy() }
/** /**
* Adapter containing the list of manga from the catalogue. * Set the title of controller.
*/ *
private var adapter: FlexibleAdapter<IFlexible<*>>? = null * @return title.
*/
/** override fun getTitle(): String? {
* Snackbar containing an error message when a request fails. return applicationContext?.getString(R.string.label_catalogues)
*/ }
private var snack: Snackbar? = null
/**
/** * Create the [CataloguePresenter] used in controller.
* Navigation view containing filter items. *
*/ * @return instance of [CataloguePresenter]
private var navView: CatalogueNavigationView? = null */
override fun createPresenter(): CataloguePresenter {
/** return CataloguePresenter()
* Recycler view with the list of results. }
*/
private var recycler: RecyclerView? = null /**
* Initiate the view with [R.layout.catalogue_main_controller].
/** *
* Drawer listener to allow swipe only for closing the drawer. * @param inflater used to load the layout xml.
*/ * @param container containing parent views.
private var drawerListener: DrawerLayout.DrawerListener? = null * @return inflated view.
*/
/** override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
* Subscription for the search view. return inflater.inflate(R.layout.catalogue_main_controller, container, false)
*/ }
private var searchViewSubscription: Subscription? = null
/**
/** * Called when the view is created
* Subscription for the number of manga per row. *
*/ * @param view view of controller
private var numColumnsSubscription: Subscription? = null */
override fun onViewCreated(view: View) {
/** super.onViewCreated(view)
* Endless loading item.
*/ adapter = CatalogueAdapter(this)
private var progressItem: ProgressItem? = null
// Create recycler and set adapter.
init { recycler.layoutManager = LinearLayoutManager(view.context)
setHasOptionsMenu(true) recycler.adapter = adapter
} recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
}
override fun getTitle(): String? {
return presenter.source.name override fun onDestroyView(view: View) {
} adapter = null
super.onDestroyView(view)
override fun createPresenter(): CataloguePresenter { }
return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
} override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
return inflater.inflate(R.layout.catalogue_controller, container, false) presenter.updateSources()
} }
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState) /**
* Called when login dialog is closed, refreshes the adapter.
// Initialize adapter, scroll listener and recycler views *
adapter = FlexibleAdapter(null, this) * @param source clicked item containing source information.
setupRecycler(view) */
override fun loginDialogClosed(source: LoginSource) {
navView?.setFilters(presenter.filterItems) if (source.isLogged()) {
adapter?.clear()
view.progress?.visible() presenter.loadSources()
} }
}
override fun onDestroyView(view: View) {
super.onDestroyView(view) /**
numColumnsSubscription?.unsubscribe() * Called when item is clicked
numColumnsSubscription = null */
searchViewSubscription?.unsubscribe() override fun onItemClick(position: Int): Boolean {
searchViewSubscription = null val item = adapter?.getItem(position) as? SourceItem ?: return false
adapter = null val source = item.source
snack = null if (source is LoginSource && !source.isLogged()) {
recycler = null val dialog = SourceLoginDialog(source)
} dialog.targetController = this
dialog.showDialog(router)
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? { } else {
// Inflate and prepare drawer // Open the catalogue view.
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView openCatalogue(source, BrowseCatalogueController(source))
this.navView = navView }
drawerListener = DrawerSwipeCloseListener(drawer, navView).also { return false
drawer.addDrawerListener(it) }
}
navView.setFilters(presenter.filterItems) /**
* Called when browse is clicked in [CatalogueAdapter]
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) */
override fun onBrowseClick(position: Int) {
navView.onSearchClicked = { onItemClick(position)
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() }
showProgressBar()
adapter?.clear() /**
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) * Called when latest is clicked in [CatalogueAdapter]
} */
override fun onLatestClick(position: Int) {
navView.onResetClicked = { val item = adapter?.getItem(position) as? SourceItem ?: return
presenter.appliedFilters = FilterList() openCatalogue(item.source, LatestUpdatesController(item.source))
val newFilters = presenter.source.getFilterList() }
presenter.sourceFilters = newFilters
navView.setFilters(presenter.filterItems) /**
} * Opens a catalogue with the given controller.
return navView */
} private fun openCatalogue(source: CatalogueSource, controller: BrowseCatalogueController) {
preferences.lastUsedCatalogueSource().set(source.id)
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { router.pushController(controller.withFadeTransaction())
drawerListener?.let { drawer.removeDrawerListener(it) } }
drawerListener = null
navView = null /**
} * Adds items to the options menu.
*
private fun setupRecycler(view: View) { * @param menu menu containing options.
numColumnsSubscription?.unsubscribe() * @param inflater used to load the menu xml.
*/
var oldPosition = RecyclerView.NO_POSITION override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
val oldRecycler = view.catalogue_view?.getChildAt(1) // Inflate menu
if (oldRecycler is RecyclerView) { inflater.inflate(R.menu.catalogue_main, menu)
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null // Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
view.catalogue_view?.removeView(oldRecycler) val searchView = searchItem.actionView as SearchView
}
// Change hint to show global search.
val recycler = if (presenter.isListMode) { searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
RecyclerView(view.context).apply {
id = R.id.recycler // Create query listener which opens the global search view.
layoutManager = LinearLayoutManager(context) searchView.queryTextChangeEvents()
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) .filter { it.isSubmitted }
} .subscribeUntilDestroy {
} else { val query = it.queryText().toString()
(view.catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply { router.pushController(CatalogueSearchController(query).withFadeTransaction())
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() }
.doOnNext { spanCount = it } }
.skip(1)
// Set again the adapter to recalculate the covers height /**
.subscribe { adapter = this@CatalogueController.adapter } * Called when an option menu item has been selected by the user.
*
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { * @param item The selected item.
override fun getSpanSize(position: Int): Int { * @return True if this event has been consumed, false if it has not.
return when (adapter?.getItemViewType(position)) { */
R.layout.catalogue_grid_item, null -> 1 override fun onOptionsItemSelected(item: MenuItem): Boolean {
else -> spanCount when (item.itemId) {
} // Initialize option to open catalogue settings.
} R.id.action_settings -> {
} router.pushController((RouterTransaction.with(SettingsSourcesController()))
} .popChangeHandler(SettingsSourcesFadeChangeHandler())
} .pushChangeHandler(FadeChangeHandler()))
recycler.setHasFixedSize(true) }
recycler.adapter = adapter else -> return super.onOptionsItemSelected(item)
}
view.catalogue_view.addView(recycler, 1) return true
}
if (oldPosition != RecyclerView.NO_POSITION) {
recycler.layoutManager.scrollToPosition(oldPosition) /**
} * Called to update adapter containing sources.
this.recycler = recycler */
} fun setSources(sources: List<IFlexible<*>>) {
adapter?.updateDataSet(sources)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { }
inflater.inflate(R.menu.catalogue_list, menu)
/**
// Initialize search menu * Called to set the last used catalogue at the top of the view.
menu.findItem(R.id.action_search).apply { */
val searchView = actionView as SearchView fun setLastUsedSource(item: SourceItem?) {
adapter?.removeAllScrollableHeaders()
val query = presenter.query if (item != null) {
if (!query.isBlank()) { adapter?.addScrollableHeader(item)
expandActionView() }
searchView.setQuery(query, true) }
searchView.clearFocus()
} class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
}
val searchEventsObservable = searchView.queryTextChangeEvents()
.skip(1)
.share()
val writingObservable = searchEventsObservable
.filter { !it.isSubmitted }
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
val submitObservable = searchEventsObservable
.filter { it.isSubmitted }
searchViewSubscription?.unsubscribe()
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
.map { it.queryText().toString() }
.distinctUntilChanged()
.subscribeUntilDestroy { searchWithQuery(it) }
untilDestroySubscriptions.add(
Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
}
// Setup filters button
menu.findItem(R.id.action_set_filter).apply {
icon.mutate()
if (presenter.sourceFilters.isEmpty()) {
isEnabled = false
icon.alpha = 128
} else {
isEnabled = true
icon.alpha = 255
}
}
// Show next display mode
menu.findItem(R.id.action_display_mode).apply {
val icon = if (presenter.isListMode)
R.drawable.ic_view_module_white_24dp
else
R.drawable.ic_view_list_white_24dp
setIcon(icon)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Restarts the request with a new query.
*
* @param newQuery the new query.
*/
private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing
if (presenter.query == newQuery)
return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
if (newQuery == "") {
activity?.invalidateOptionsMenu()
}
showProgressBar()
adapter?.clear()
presenter.restartPager(newQuery)
}
/**
* Called from the presenter when the network request is received.
*
* @param page the current page.
* @param mangas the list of manga of the page.
*/
fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
val adapter = adapter ?: return
hideProgressBar()
if (page == 1) {
adapter.clear()
resetProgressItem()
}
adapter.onLoadMoreComplete(mangas)
}
/**
* Called from the presenter when the network request fails.
*
* @param error the error received.
*/
fun onAddPageError(error: Throwable) {
Timber.e(error)
val adapter = adapter ?: return
adapter.onLoadMoreComplete(null)
hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss()
snack = view?.catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar.
if (adapter.mainItemCount > 0) {
val item = progressItem ?: return@setAction
adapter.addScrollableFooterWithDelay(item, 0, true)
} else {
showProgressBar()
}
presenter.requestNext()
}
}
}
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter?.endlessTargetCount = 0
adapter?.setEndlessScrollListener(this, progressItem!!)
}
/**
* Called by the adapter when scrolled near the bottom.
*/
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
Timber.e("onLoadMore")
if (presenter.hasNextPage()) {
presenter.requestNext()
} else {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
}
}
override fun noMoreLoad(newItemsSize: Int) {
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the manga initialized
*/
fun onMangaInitialized(manga: Manga) {
getHolder(manga)?.setImage(manga)
}
/**
* Swaps the current display mode.
*/
fun swapDisplayMode() {
val view = view ?: return
val adapter = adapter ?: return
presenter.swapDisplayMode()
val isListMode = presenter.isListMode
activity?.invalidateOptionsMenu()
setupRecycler(view)
if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
// Initialize mangas if going to grid view or if over wifi when going to list view
val mangas = (0..adapter.itemCount-1).mapNotNull {
(adapter.getItem(it) as? CatalogueItem)?.manga
}
presenter.initializeMangas(mangas)
}
}
/**
* Returns a preference for the number of manga per row based on the current orientation.
*
* @return the preference.
*/
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
preferences.portraitColumns()
else
preferences.landscapeColumns()
}
/**
* Returns the view holder for the given manga.
*
* @param manga the manga to find.
* @return the holder of the manga or null if it's not bound.
*/
private fun getHolder(manga: Manga): CatalogueHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
if (item != null && item.manga.id!! == manga.id!!) {
return holder as CatalogueHolder
}
}
return null
}
/**
* Shows the progress bar.
*/
private fun showProgressBar() {
view?.progress?.visible()
snack?.dismiss()
snack = null
}
/**
* Hides active progress bars.
*/
private fun hideProgressBar() {
view?.progress?.gone()
}
/**
* Called when a manga is clicked.
*
* @param position the position of the element clicked.
* @return true if the item should be selected, false otherwise.
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? CatalogueItem ?: return false
router.pushController(RouterTransaction.with(MangaController(item.manga, true))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
return false
}
/**
* Called when a manga is long clicked.
*
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
* in, the list consists of the default category plus the user's categories. The default category is preselected on
* new manga, and on already favorited manga the manga's categories are preselected.
*
* @param position the position of the element clicked.
*/
override fun onItemLongClick(position: Int) {
val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
if (manga.favorite) {
MaterialDialog.Builder(activity!!)
.items(resources?.getString(R.string.remove_from_library))
.itemsCallback { _, _, which, _ ->
when (which) {
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
}
}
}.show()
} else {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
val categories = presenter.getCategories()
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
if (defaultCategory != null) {
presenter.moveMangaToCategory(manga, defaultCategory)
} else if (categories.size <= 1) { // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull())
} else {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router)
}
}
}
/**
* Update manga to use selected categories.
*
* @param mangas The list of manga to move to categories.
* @param categories The list of categories where manga will be placed.
*/
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
val manga = mangas.firstOrNull() ?: return
presenter.updateMangaCategories(manga, categories)
}
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
}
}

View File

@ -1,376 +1,104 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.os.Bundle import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.TimeUnit
/** /**
* Presenter of [CatalogueController]. * Presenter of [CatalogueController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param sourceManager manages the different sources.
* @param preferences application preferences.
*/ */
open class CataloguePresenter( class CataloguePresenter(
sourceId: Long, val sourceManager: SourceManager = Injekt.get(),
sourceManager: SourceManager = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get()
private val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<CatalogueController>() { ) : BasePresenter<CatalogueController>() {
/** /**
* Selected source. * Enabled sources.
*/ */
val source = sourceManager.get(sourceId) as CatalogueSource var sources = getEnabledSources()
/** /**
* Query from the view. * Subscription for retrieving enabled sources.
*/ */
var query = "" private var sourceSubscription: Subscription? = null
private set
/**
* Modifiable list of filters.
*/
var sourceFilters = FilterList()
set(value) {
field = value
filterItems = value.toItems()
}
var filterItems: List<IFlexible<*>> = emptyList()
/**
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
*/
var appliedFilters = FilterList()
/**
* Pager containing a list of manga results.
*/
private lateinit var pager: Pager
/**
* Subject that initializes a list of manga.
*/
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
/**
* Whether the view is in list mode or not.
*/
var isListMode: Boolean = false
private set
/**
* Subscription for the pager.
*/
private var pagerSubscription: Subscription? = null
/**
* Subscription for one request from the pager.
*/
private var pageSubscription: Subscription? = null
/**
* Subscription to initialize manga details.
*/
private var initializerSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
sourceFilters = source.getFilterList() // Load enabled and last used sources
loadSources()
if (savedState != null) { loadLastUsedSource()
query = savedState.getString(CataloguePresenter::query.name, "")
}
add(prefs.catalogueAsList().asObservable()
.subscribe { setDisplayMode(it) })
restartPager()
}
override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query)
super.onSave(state)
} }
/** /**
* Restarts the pager for the active source with the provided query and filters. * Unsubscribe and create a new subscription to fetch enabled sources.
*
* @param query the query.
* @param filters the current state of the filters (for search mode).
*/ */
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) { fun loadSources() {
this.query = query sourceSubscription?.unsubscribe()
this.appliedFilters = filters
subscribeToMangaInitializer() val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
// Catalogues without a lang defined will be placed at the end
// Create a new pager. when {
pager = createPager(query, filters) d1 == "" && d2 != "" -> 1
d2 == "" && d1 != "" -> -1
val sourceId = source.id else -> d1.compareTo(d2)
val catalogueAsList = prefs.catalogueAsList()
// Prepare the pager.
pagerSubscription?.let { remove(it) }
pagerSubscription = pager.results()
.observeOn(Schedulers.io())
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
.doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } }
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, (page, mangas) ->
view.onAddPage(page, mangas)
}, { _, error ->
Timber.e(error)
})
// Request first page.
requestNext()
}
/**
* Requests the next page for the active pager.
*/
fun requestNext() {
if (!hasNextPage()) return
pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted.
}, CatalogueController::onAddPageError)
}
/**
* Returns true if the last fetched page has a next page.
*/
fun hasNextPage(): Boolean {
return pager.hasNextPage
}
/**
* Sets the display mode.
*
* @param asList whether the current mode is in list or not.
*/
private fun setDisplayMode(asList: Boolean) {
isListMode = asList
subscribeToMangaInitializer()
}
/**
* Subscribes to the initializer of manga details and updates the view if needed.
*/
private fun subscribeToMangaInitializer() {
initializerSubscription?.let { remove(it) }
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) }
.filter { it.thumbnail_url == null && !it.initialized }
.concatMap { getMangaDetailsObservable(it) }
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ manga ->
@Suppress("DEPRECATION")
view?.onMangaInitialized(manga)
}, { error ->
Timber.e(error)
})
.apply { add(this) }
}
/**
* Returns a manga from the database for the given manga from network. It creates a new entry
* if the manga is not yet in the database.
*
* @param sManga the manga from the source.
* @return a manga from the database.
*/
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) {
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
newManga.copyFrom(sManga)
val result = db.insertManga(newManga).executeAsBlocking()
newManga.id = result.insertedId()
localManga = newManga
}
return localManga
}
/**
* Initialize a list of manga.
*
* @param mangas the list of manga to initialize.
*/
fun initializeMangas(mangas: List<Manga>) {
mangaDetailSubject.onNext(mangas)
}
/**
* Returns an observable of manga that initializes the given manga.
*
* @param manga the manga to initialize.
* @return an observable of the manga to initialize
*/
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
return source.fetchMangaDetails(manga)
.flatMap { networkManga ->
manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
Observable.just(manga)
}
.onErrorResumeNext { Observable.just(manga) }
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
manga.favorite = !manga.favorite
if (!manga.favorite) {
coverCache.deleteFromCache(manga.thumbnail_url)
}
db.insertManga(manga).executeAsBlocking()
}
/**
* Changes the active display mode.
*/
fun swapDisplayMode() {
prefs.catalogueAsList().set(!isListMode)
}
/**
* Set the filter states for the current source.
*
* @param filters a list of active filters.
*/
fun setSourceFilter(filters: FilterList) {
restartPager(filters = filters)
}
open fun createPager(query: String, filters: FilterList): Pager {
return CataloguePager(source, query, filters)
}
private fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull {
when (it) {
is Filter.Header -> HeaderItem(it)
is Filter.Separator -> SeparatorItem(it)
is Filter.CheckBox -> CheckboxItem(it)
is Filter.TriState -> TriStateItem(it)
is Filter.Text -> TextItem(it)
is Filter.Select<*> -> SelectItem(it)
is Filter.Group<*> -> {
val group = GroupItem(it)
val subItems = it.state.mapNotNull {
when (it) {
is Filter.CheckBox -> CheckboxSectionItem(it)
is Filter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it)
else -> null
} as? ISectionable<*, *>
}
subItems.forEach { it.header = group }
group.subItems = subItems
group
}
is Filter.Sort -> {
val group = SortGroup(it)
val subItems = it.values.map {
SortItem(it, group)
}
group.subItems = subItems
group
}
} }
} }
} val byLang = sources.groupByTo(map, { it.lang })
val sourceItems = byLang.flatMap {
/** val langItem = LangItem(it.key)
* Get the default, and user categories. it.value.map { source -> SourceItem(source, langItem) }
*
* @return List of categories, default plus user categories
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
}
/**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
*
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
}
/**
* Move the given manga to categories.
*
* @param categories the selected categories.
* @param manga the manga to move.
*/
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
}
/**
* Move the given manga to the category.
*
* @param category the selected category.
* @param manga the manga to move.
*/
fun moveMangaToCategory(manga: Manga, category: Category?) {
moveMangaToCategories(manga, listOfNotNull(category))
}
/**
* Update manga to use selected categories.
*
* @param manga needed to change
* @param selectedCategories selected categories
*/
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
if (!selectedCategories.isEmpty()) {
if (!manga.favorite)
changeMangaFavorite(manga)
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
changeMangaFavorite(manga)
} }
sourceSubscription = Observable.just(sourceItems)
.subscribeLatestCache(CatalogueController::setSources)
} }
private fun loadLastUsedSource() {
val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
// Emit the first item immediately but delay subsequent emissions by 500ms.
Observable.merge(
sharedObs.take(1),
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
.distinctUntilChanged()
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
.subscribeLatestCache(CatalogueController::setLastUsedSource)
}
fun updateSources() {
sources = getEnabledSources()
loadSources()
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<CatalogueSource> {
val languages = preferences.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" } +
sourceManager.get(LocalSource.ID) as LocalSource
}
} }

View File

@ -1,21 +1,22 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.* import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
import java.util.* import java.util.*
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) { class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
BaseFlexibleViewHolder(view, adapter, true) {
fun bind(item: LangItem) {
itemView.title.text = when { fun bind(item: LangItem) {
item.code == "" -> itemView.context.getString(R.string.other_source) title.text = when {
else -> { item.code == "" -> itemView.context.getString(R.string.other_source)
val locale = Locale(item.code) else -> {
locale.getDisplayName(locale).capitalize() val locale = Locale(item.code)
} locale.getDisplayName(locale).capitalize()
} }
} }
}
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter

View File

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.ui.catalogue
class NoResultsException : Exception()

View File

@ -1,47 +1,44 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val divider: Drawable private val divider: Drawable
init { init {
val a = context.obtainStyledAttributes(ATTRS) val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
divider = a.getDrawable(0) divider = a.getDrawable(0)
a.recycle() a.recycle()
} }
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + SourceHolder.margin val left = parent.paddingLeft + SourceHolder.margin
val right = parent.width - parent.paddingRight - SourceHolder.margin val right = parent.width - parent.paddingRight - SourceHolder.margin
val childCount = parent.childCount val childCount = parent.childCount
for (i in 0 until childCount - 1) { for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i) val child = parent.getChildAt(i)
if (parent.getChildViewHolder(child) is SourceHolder && if (parent.getChildViewHolder(child) is SourceHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) { parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
val params = child.layoutParams as RecyclerView.LayoutParams val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom) divider.setBounds(left, top, right, bottom)
divider.draw(c) divider.draw(c)
} }
} }
} }
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) { state: RecyclerView.State) {
outRect.set(0, 0, 0, divider.intrinsicHeight) outRect.set(0, 0, 0, divider.intrinsicHeight)
} }
companion object {
private val ATTRS = intArrayOf(android.R.attr.listDivider)
}
} }

View File

@ -1,55 +1,53 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.dpToPx import eu.kanade.tachiyomi.util.dpToPx
import eu.kanade.tachiyomi.util.getRound import eu.kanade.tachiyomi.util.getRound
import eu.kanade.tachiyomi.util.gone import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.visible import eu.kanade.tachiyomi.util.visible
import io.github.mthli.slice.Slice import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.* import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) { class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHolder(view, adapter) {
private val slice = Slice(itemView.card).apply { private val slice = Slice(card).apply {
setColor(adapter.cardBackground) setColor(adapter.cardBackground)
} }
init { init {
itemView.source_browse.setOnClickListener { source_browse.setOnClickListener {
adapter.browseClickListener.onBrowseClick(adapterPosition) adapter.browseClickListener.onBrowseClick(adapterPosition)
} }
itemView.source_latest.setOnClickListener { source_latest.setOnClickListener {
adapter.latestClickListener.onLatestClick(adapterPosition) adapter.latestClickListener.onLatestClick(adapterPosition)
} }
} }
fun bind(item: SourceItem) { fun bind(item: SourceItem) {
val source = item.source val source = item.source
with(itemView) { setCardEdges(item)
setCardEdges(item)
// Set source name // Set source name
title.text = source.name title.text = source.name
// Set circle letter image. // Set circle letter image.
post { itemView.post {
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
} }
// If source is login, show only login option // If source is login, show only login option
if (source is LoginSource && !source.isLogged()) { if (source is LoginSource && !source.isLogged()) {
source_browse.setText(R.string.login) source_browse.setText(R.string.login)
source_latest.gone() source_latest.gone()
} else { } else {
source_browse.setText(R.string.browse) source_browse.setText(R.string.browse)
source_latest.visible() source_latest.visible()
}
} }
} }
@ -94,7 +92,7 @@ class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHold
} }
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) { private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
val v = itemView.card val v = card
if (v.layoutParams is ViewGroup.MarginLayoutParams) { if (v.layoutParams is ViewGroup.MarginLayoutParams) {
val p = v.layoutParams as ViewGroup.MarginLayoutParams val p = v.layoutParams as ViewGroup.MarginLayoutParams
p.setMargins(left, top, right, bottom) p.setMargins(left, top, right, bottom)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@ -26,7 +26,7 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null)
* Creates a new view holder for this item. * Creates a new view holder for this item.
*/ */
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder {
return SourceHolder(view, adapter as CatalogueMainAdapter) return SourceHolder(view, adapter as CatalogueAdapter)
} }
/** /**

View File

@ -0,0 +1,522 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.content.res.Configuration
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout
import android.support.v7.widget.*
import android.view.*
import com.afollestad.materialdialogs.MaterialDialog
import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.catalogue_controller.*
import kotlinx.android.synthetic.main.main_activity.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.Subscriptions
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
/**
* Controller to manage the catalogues available in the app.
*/
open class BrowseCatalogueController(bundle: Bundle) :
NucleusController<BrowseCataloguePresenter>(bundle),
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy()
/**
* Adapter containing the list of manga from the catalogue.
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
/**
* Snackbar containing an error message when a request fails.
*/
private var snack: Snackbar? = null
/**
* Navigation view containing filter items.
*/
private var navView: CatalogueNavigationView? = null
/**
* Recycler view with the list of results.
*/
private var recycler: RecyclerView? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null
/**
* Subscription for the search view.
*/
private var searchViewSubscription: Subscription? = null
/**
* Subscription for the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null
init {
setHasOptionsMenu(true)
}
override fun getTitle(): String? {
return presenter.source.name
}
override fun createPresenter(): BrowseCataloguePresenter {
return BrowseCataloguePresenter(args.getLong(SOURCE_ID_KEY))
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_controller, container, false)
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Initialize adapter, scroll listener and recycler views
adapter = FlexibleAdapter(null, this)
setupRecycler(view)
navView?.setFilters(presenter.filterItems)
progress?.visible()
}
override fun onDestroyView(view: View) {
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null
snack = null
recycler = null
super.onDestroyView(view)
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
// Inflate and prepare drawer
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
this.navView = navView
drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
drawer.addDrawerListener(it)
}
navView.setFilters(presenter.filterItems)
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar()
adapter?.clear()
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
}
navView.onResetClicked = {
presenter.appliedFilters = FilterList()
val newFilters = presenter.source.getFilterList()
presenter.sourceFilters = newFilters
navView.setFilters(presenter.filterItems)
}
return navView
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
drawerListener?.let { drawer.removeDrawerListener(it) }
drawerListener = null
navView = null
}
private fun setupRecycler(view: View) {
numColumnsSubscription?.unsubscribe()
var oldPosition = RecyclerView.NO_POSITION
val oldRecycler = catalogue_view?.getChildAt(1)
if (oldRecycler is RecyclerView) {
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null
catalogue_view?.removeView(oldRecycler)
}
val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
} else {
(catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { spanCount = it }
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribe { adapter = this@BrowseCatalogueController.adapter }
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (adapter?.getItemViewType(position)) {
R.layout.catalogue_grid_item, null -> 1
else -> spanCount
}
}
}
}
}
recycler.setHasFixedSize(true)
recycler.adapter = adapter
catalogue_view.addView(recycler, 1)
if (oldPosition != RecyclerView.NO_POSITION) {
recycler.layoutManager.scrollToPosition(oldPosition)
}
this.recycler = recycler
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.catalogue_list, menu)
// Initialize search menu
menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView
val query = presenter.query
if (!query.isBlank()) {
expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
val searchEventsObservable = searchView.queryTextChangeEvents()
.skip(1)
.share()
val writingObservable = searchEventsObservable
.filter { !it.isSubmitted }
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
val submitObservable = searchEventsObservable
.filter { it.isSubmitted }
searchViewSubscription?.unsubscribe()
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
.map { it.queryText().toString() }
.distinctUntilChanged()
.subscribeUntilDestroy { searchWithQuery(it) }
untilDestroySubscriptions.add(
Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
}
// Setup filters button
menu.findItem(R.id.action_set_filter).apply {
icon.mutate()
if (presenter.sourceFilters.isEmpty()) {
isEnabled = false
icon.alpha = 128
} else {
isEnabled = true
icon.alpha = 255
}
}
// Show next display mode
menu.findItem(R.id.action_display_mode).apply {
val icon = if (presenter.isListMode)
R.drawable.ic_view_module_white_24dp
else
R.drawable.ic_view_list_white_24dp
setIcon(icon)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Restarts the request with a new query.
*
* @param newQuery the new query.
*/
private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing
if (presenter.query == newQuery)
return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
if (newQuery == "") {
activity?.invalidateOptionsMenu()
}
showProgressBar()
adapter?.clear()
presenter.restartPager(newQuery)
}
/**
* Called from the presenter when the network request is received.
*
* @param page the current page.
* @param mangas the list of manga of the page.
*/
fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
val adapter = adapter ?: return
hideProgressBar()
if (page == 1) {
adapter.clear()
resetProgressItem()
}
adapter.onLoadMoreComplete(mangas)
}
/**
* Called from the presenter when the network request fails.
*
* @param error the error received.
*/
fun onAddPageError(error: Throwable) {
Timber.e(error)
val adapter = adapter ?: return
adapter.onLoadMoreComplete(null)
hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss()
snack = catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar.
if (adapter.mainItemCount > 0) {
val item = progressItem ?: return@setAction
adapter.addScrollableFooterWithDelay(item, 0, true)
} else {
showProgressBar()
}
presenter.requestNext()
}
}
}
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter?.endlessTargetCount = 0
adapter?.setEndlessScrollListener(this, progressItem!!)
}
/**
* Called by the adapter when scrolled near the bottom.
*/
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
if (presenter.hasNextPage()) {
presenter.requestNext()
} else {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
}
}
override fun noMoreLoad(newItemsSize: Int) {
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the manga initialized
*/
fun onMangaInitialized(manga: Manga) {
getHolder(manga)?.setImage(manga)
}
/**
* Swaps the current display mode.
*/
fun swapDisplayMode() {
val view = view ?: return
val adapter = adapter ?: return
presenter.swapDisplayMode()
val isListMode = presenter.isListMode
activity?.invalidateOptionsMenu()
setupRecycler(view)
if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
// Initialize mangas if going to grid view or if over wifi when going to list view
val mangas = (0 until adapter.itemCount).mapNotNull {
(adapter.getItem(it) as? CatalogueItem)?.manga
}
presenter.initializeMangas(mangas)
}
}
/**
* Returns a preference for the number of manga per row based on the current orientation.
*
* @return the preference.
*/
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
preferences.portraitColumns()
else
preferences.landscapeColumns()
}
/**
* Returns the view holder for the given manga.
*
* @param manga the manga to find.
* @return the holder of the manga or null if it's not bound.
*/
private fun getHolder(manga: Manga): CatalogueHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
if (item != null && item.manga.id!! == manga.id!!) {
return holder as CatalogueHolder
}
}
return null
}
/**
* Shows the progress bar.
*/
private fun showProgressBar() {
progress?.visible()
snack?.dismiss()
snack = null
}
/**
* Hides active progress bars.
*/
private fun hideProgressBar() {
progress?.gone()
}
/**
* Called when a manga is clicked.
*
* @param position the position of the element clicked.
* @return true if the item should be selected, false otherwise.
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? CatalogueItem ?: return false
router.pushController(MangaController(item.manga, true).withFadeTransaction())
return false
}
/**
* Called when a manga is long clicked.
*
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
* in, the list consists of the default category plus the user's categories. The default category is preselected on
* new manga, and on already favorited manga the manga's categories are preselected.
*
* @param position the position of the element clicked.
*/
override fun onItemLongClick(position: Int) {
val activity = activity ?: return
val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
if (manga.favorite) {
MaterialDialog.Builder(activity)
.items(activity.getString(R.string.remove_from_library))
.itemsCallback { _, _, which, _ ->
when (which) {
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
activity?.toast(activity?.getString(R.string.manga_removed_library))
}
}
}.show()
} else {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
val categories = presenter.getCategories()
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
if (defaultCategory != null) {
presenter.moveMangaToCategory(manga, defaultCategory)
} else if (categories.size <= 1) { // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull())
} else {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router)
}
activity?.toast(activity?.getString(R.string.manga_added_library))
}
}
/**
* Update manga to use selected categories.
*
* @param mangas The list of manga to move to categories.
* @param categories The list of categories where manga will be placed.
*/
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
val manga = mangas.firstOrNull() ?: return
presenter.updateMangaCategories(manga, categories)
}
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
}
}

View File

@ -0,0 +1,376 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [BrowseCatalogueController].
*/
open class BrowseCataloguePresenter(
sourceId: Long,
sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<BrowseCatalogueController>() {
/**
* Selected source.
*/
val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Query from the view.
*/
var query = ""
private set
/**
* Modifiable list of filters.
*/
var sourceFilters = FilterList()
set(value) {
field = value
filterItems = value.toItems()
}
var filterItems: List<IFlexible<*>> = emptyList()
/**
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
*/
var appliedFilters = FilterList()
/**
* Pager containing a list of manga results.
*/
private lateinit var pager: Pager
/**
* Subject that initializes a list of manga.
*/
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
/**
* Whether the view is in list mode or not.
*/
var isListMode: Boolean = false
private set
/**
* Subscription for the pager.
*/
private var pagerSubscription: Subscription? = null
/**
* Subscription for one request from the pager.
*/
private var pageSubscription: Subscription? = null
/**
* Subscription to initialize manga details.
*/
private var initializerSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
sourceFilters = source.getFilterList()
if (savedState != null) {
query = savedState.getString(::query.name, "")
}
add(prefs.catalogueAsList().asObservable()
.subscribe { setDisplayMode(it) })
restartPager()
}
override fun onSave(state: Bundle) {
state.putString(::query.name, query)
super.onSave(state)
}
/**
* Restarts the pager for the active source with the provided query and filters.
*
* @param query the query.
* @param filters the current state of the filters (for search mode).
*/
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
this.query = query
this.appliedFilters = filters
subscribeToMangaInitializer()
// Create a new pager.
pager = createPager(query, filters)
val sourceId = source.id
val catalogueAsList = prefs.catalogueAsList()
// Prepare the pager.
pagerSubscription?.let { remove(it) }
pagerSubscription = pager.results()
.observeOn(Schedulers.io())
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
.doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } }
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, (page, mangas) ->
view.onAddPage(page, mangas)
}, { _, error ->
Timber.e(error)
})
// Request first page.
requestNext()
}
/**
* Requests the next page for the active pager.
*/
fun requestNext() {
if (!hasNextPage()) return
pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted.
}, BrowseCatalogueController::onAddPageError)
}
/**
* Returns true if the last fetched page has a next page.
*/
fun hasNextPage(): Boolean {
return pager.hasNextPage
}
/**
* Sets the display mode.
*
* @param asList whether the current mode is in list or not.
*/
private fun setDisplayMode(asList: Boolean) {
isListMode = asList
subscribeToMangaInitializer()
}
/**
* Subscribes to the initializer of manga details and updates the view if needed.
*/
private fun subscribeToMangaInitializer() {
initializerSubscription?.let { remove(it) }
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) }
.filter { it.thumbnail_url == null && !it.initialized }
.concatMap { getMangaDetailsObservable(it) }
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ manga ->
@Suppress("DEPRECATION")
view?.onMangaInitialized(manga)
}, { error ->
Timber.e(error)
})
.apply { add(this) }
}
/**
* Returns a manga from the database for the given manga from network. It creates a new entry
* if the manga is not yet in the database.
*
* @param sManga the manga from the source.
* @return a manga from the database.
*/
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) {
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
newManga.copyFrom(sManga)
val result = db.insertManga(newManga).executeAsBlocking()
newManga.id = result.insertedId()
localManga = newManga
}
return localManga
}
/**
* Initialize a list of manga.
*
* @param mangas the list of manga to initialize.
*/
fun initializeMangas(mangas: List<Manga>) {
mangaDetailSubject.onNext(mangas)
}
/**
* Returns an observable of manga that initializes the given manga.
*
* @param manga the manga to initialize.
* @return an observable of the manga to initialize
*/
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
return source.fetchMangaDetails(manga)
.flatMap { networkManga ->
manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
Observable.just(manga)
}
.onErrorResumeNext { Observable.just(manga) }
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
manga.favorite = !manga.favorite
if (!manga.favorite) {
coverCache.deleteFromCache(manga.thumbnail_url)
}
db.insertManga(manga).executeAsBlocking()
}
/**
* Changes the active display mode.
*/
fun swapDisplayMode() {
prefs.catalogueAsList().set(!isListMode)
}
/**
* Set the filter states for the current source.
*
* @param filters a list of active filters.
*/
fun setSourceFilter(filters: FilterList) {
restartPager(filters = filters)
}
open fun createPager(query: String, filters: FilterList): Pager {
return CataloguePager(source, query, filters)
}
private fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull {
when (it) {
is Filter.Header -> HeaderItem(it)
is Filter.Separator -> SeparatorItem(it)
is Filter.CheckBox -> CheckboxItem(it)
is Filter.TriState -> TriStateItem(it)
is Filter.Text -> TextItem(it)
is Filter.Select<*> -> SelectItem(it)
is Filter.Group<*> -> {
val group = GroupItem(it)
val subItems = it.state.mapNotNull {
when (it) {
is Filter.CheckBox -> CheckboxSectionItem(it)
is Filter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it)
else -> null
} as? ISectionable<*, *>
}
subItems.forEach { it.header = group }
group.subItems = subItems
group
}
is Filter.Sort -> {
val group = SortGroup(it)
val subItems = it.values.map {
SortItem(it, group)
}
group.subItems = subItems
group
}
}
}
}
/**
* Get the default, and user categories.
*
* @return List of categories, default plus user categories
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
}
/**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
*
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
}
/**
* Move the given manga to categories.
*
* @param categories the selected categories.
* @param manga the manga to move.
*/
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
}
/**
* Move the given manga to the category.
*
* @param category the selected category.
* @param manga the manga to move.
*/
fun moveMangaToCategory(manga: Manga, category: Category?) {
moveMangaToCategories(manga, listOfNotNull(category))
}
/**
* Update manga to use selected categories.
*
* @param manga needed to change
* @param selectedCategories selected categories
*/
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
if (!selectedCategories.isEmpty()) {
if (!manga.favorite)
changeMangaFavorite(manga)
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
changeMangaFavorite(manga)
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -6,7 +6,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.catalogue_grid_item.*
/** /**
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
@ -27,16 +27,16 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
*/ */
override fun onSetValues(manga: Manga) { override fun onSetValues(manga: Manga) {
// Set manga title // Set manga title
view.title.text = manga.title title.text = manga.title
// Set alpha of thumbnail. // Set alpha of thumbnail.
view.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
setImage(manga) setImage(manga)
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(view.thumbnail) GlideApp.with(view.context).clear(thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
@ -44,7 +44,7 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
.centerCrop() .centerCrop()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(StateImageViewTarget(view.thumbnail, view.progress)) .into(StateImageViewTarget(thumbnail, progress))
} }
} }
} }

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
/** /**
* Generic class used to hold the displayed data of a manga in the catalogue. * Generic class used to hold the displayed data of a manga in the catalogue.
@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
* @param adapter the adapter handling this holder. * @param adapter the adapter handling this holder.
*/ */
abstract class CatalogueHolder(view: View, adapter: FlexibleAdapter<*>) : abstract class CatalogueHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) { BaseFlexibleViewHolder(view, adapter) {
/** /**
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -6,7 +6,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import kotlinx.android.synthetic.main.catalogue_list_item.view.* import kotlinx.android.synthetic.main.catalogue_list_item.*
/** /**
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
@ -29,14 +29,14 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
* @param manga the manga to bind. * @param manga the manga to bind.
*/ */
override fun onSetValues(manga: Manga) { override fun onSetValues(manga: Manga) {
view.title.text = manga.title title.text = manga.title
view.title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor) title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
setImage(manga) setImage(manga)
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(view.thumbnail) GlideApp.with(view.context).clear(thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
@ -46,7 +46,7 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
.dontAnimate() .dontAnimate()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(view.thumbnail) .into(thumbnail)
} }
} }

View File

@ -1,40 +1,40 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.ViewGroup import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.SimpleNavigationView import eu.kanade.tachiyomi.widget.SimpleNavigationView
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.* import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: SimpleNavigationView(context, attrs) { : SimpleNavigationView(context, attrs) {
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null) val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
.setDisplayHeadersAtStartUp(true) .setDisplayHeadersAtStartUp(true)
.setStickyHeaders(true) .setStickyHeaders(true)
var onSearchClicked = {} var onSearchClicked = {}
var onResetClicked = {} var onResetClicked = {}
init { init {
recycler.adapter = adapter recycler.adapter = adapter
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
val view = inflate(R.layout.catalogue_drawer_content) val view = inflate(R.layout.catalogue_drawer_content)
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler) ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
addView(view) addView(view)
search_btn.setOnClickListener { onSearchClicked() } search_btn.setOnClickListener { onSearchClicked() }
reset_btn.setOnClickListener { onResetClicked() } reset_btn.setOnClickListener { onResetClicked() }
} }
fun setFilters(items: List<IFlexible<*>>) { fun setFilters(items: List<IFlexible<*>>) {
adapter.updateDataSet(items) adapter.updateDataSet(items)
} }
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList

View File

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
class NoResultsException : Exception()

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View import android.view.View
import android.widget.ProgressBar import android.widget.ProgressBar

View File

@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.* import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.*
class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter) class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
: FlexibleViewHolder(view, adapter) { : BaseFlexibleViewHolder(view, adapter) {
init { init {
// Call onMangaClickListener when item is pressed. // Call onMangaClickListener when item is pressed.
@ -22,13 +22,13 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
} }
fun bind(manga: Manga) { fun bind(manga: Manga) {
itemView.tvTitle.text = manga.title tvTitle.text = manga.title
setImage(manga) setImage(manga)
} }
fun setImage(manga: Manga) { fun setImage(manga: Manga) {
GlideApp.with(itemView.context).clear(itemView.itemImage) GlideApp.with(itemView.context).clear(itemImage)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context) GlideApp.with(itemView.context)
.load(manga) .load(manga)
@ -36,7 +36,7 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
.centerCrop() .centerCrop()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(StateImageViewTarget(itemView.itemImage, itemView.progress)) .into(StateImageViewTarget(itemImage, progress))
} }
} }

View File

@ -4,15 +4,14 @@ import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.* import kotlinx.android.synthetic.main.catalogue_global_search_controller.*
/** /**
* This controller shows and manages the different search result in global search. * This controller shows and manages the different search result in global search.
@ -71,9 +70,7 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
*/ */
override fun onMangaClick(manga: Manga) { override fun onMangaClick(manga: Manga) {
// Open MangaController. // Open MangaController.
router.pushController(RouterTransaction.with(MangaController(manga, true)) router.pushController(MangaController(manga, true).withFadeTransaction())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
/** /**
@ -115,18 +112,15 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
* Called when the view is created * Called when the view is created
* *
* @param view view of controller * @param view view of controller
* @param savedViewState information from previous state.
*/ */
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
adapter = CatalogueSearchAdapter(this) adapter = CatalogueSearchAdapter(this)
with(view) { // Create recycler and set adapter.
// Create recycler and set adapter. recycler.layoutManager = LinearLayoutManager(view.context)
recycler.layoutManager = LinearLayoutManager(context) recycler.adapter = adapter
recycler.adapter = adapter
}
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {

View File

@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.view.View import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat import eu.kanade.tachiyomi.util.setVectorCompat
import eu.kanade.tachiyomi.util.visible import eu.kanade.tachiyomi.util.visible
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.* import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.*
/** /**
* Holder that binds the [CatalogueSearchItem] containing catalogue cards. * Holder that binds the [CatalogueSearchItem] containing catalogue cards.
@ -17,7 +17,8 @@ import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.vi
* @param view view of [CatalogueSearchItem] * @param view view of [CatalogueSearchItem]
* @param adapter instance of [CatalogueSearchAdapter] * @param adapter instance of [CatalogueSearchAdapter]
*/ */
class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) { class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
BaseFlexibleViewHolder(view, adapter) {
/** /**
* Adapter containing manga from search results. * Adapter containing manga from search results.
@ -27,14 +28,12 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : F
private var lastBoundResults: List<CatalogueSearchCardItem>? = null private var lastBoundResults: List<CatalogueSearchCardItem>? = null
init { init {
with(itemView) { // Set layout horizontal.
// Set layout horizontal. recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) recycler.adapter = mangaAdapter
recycler.adapter = mangaAdapter
nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp, nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
context.getResourceColor(android.R.attr.textColorHint)) view.context.getResourceColor(android.R.attr.textColorHint))
}
} }
/** /**
@ -46,28 +45,26 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : F
val source = item.source val source = item.source
val results = item.results val results = item.results
with(itemView) { // Set Title witch country code if available.
// Set Title witch country code if available. title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
when { when {
results == null -> { results == null -> {
progress.visible() progress.visible()
nothing_found.gone() nothing_found.gone()
}
results.isEmpty() -> {
progress.gone()
nothing_found.visible()
}
else -> {
progress.gone()
nothing_found.gone()
}
} }
if (results !== lastBoundResults) { results.isEmpty() -> {
mangaAdapter.updateDataSet(results) progress.gone()
lastBoundResults = results nothing_found.visible()
} }
else -> {
progress.gone()
nothing_found.gone()
}
}
if (results !== lastBoundResults) {
mangaAdapter.updateDataSet(results)
lastBoundResults = results
} }
} }

View File

@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -67,7 +67,7 @@ class CatalogueSearchPresenter(
super.onCreate(savedState) super.onCreate(savedState)
// Perform a search with previous or initial state // Perform a search with previous or initial state
search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty()) search(savedState?.getString(BrowseCataloguePresenter::query.name) ?: initialQuery.orEmpty())
} }
override fun onDestroy() { override fun onDestroy() {
@ -77,7 +77,7 @@ class CatalogueSearchPresenter(
} }
override fun onSave(state: Bundle) { override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query) state.putString(BrowseCataloguePresenter::query.name, query)
super.onSave(state) super.onSave(state)
} }

View File

@ -1,39 +1,39 @@
package eu.kanade.tachiyomi.ui.latest_updates package eu.kanade.tachiyomi.ui.catalogue.latest
import android.os.Bundle import android.os.Bundle
import android.support.v4.widget.DrawerLayout import android.support.v4.widget.DrawerLayout
import android.view.Menu import android.view.Menu
import android.view.ViewGroup import android.view.ViewGroup
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
/** /**
* Controller that shows the latest manga from the catalogue. Inherit [CatalogueController]. * Controller that shows the latest manga from the catalogue. Inherit [BrowseCatalogueController].
*/ */
class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) { class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle) {
constructor(source: CatalogueSource) : this(Bundle().apply { constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id) putLong(SOURCE_ID_KEY, source.id)
}) })
override fun createPresenter(): CataloguePresenter { override fun createPresenter(): BrowseCataloguePresenter {
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY)) return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.action_search).isVisible = false menu.findItem(R.id.action_search).isVisible = false
menu.findItem(R.id.action_set_filter).isVisible = false menu.findItem(R.id.action_set_filter).isVisible = false
} }
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? { override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
return null return null
} }
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
} }
} }

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.latest_updates package eu.kanade.tachiyomi.ui.catalogue.latest
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.ui.catalogue.Pager import eu.kanade.tachiyomi.ui.catalogue.browse.Pager
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.ui.catalogue.latest
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.browse.Pager
/**
* Presenter of [LatestUpdatesController]. Inherit BrowseCataloguePresenter.
*/
class LatestUpdatesPresenter(sourceId: Long) : BrowseCataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source)
}
}

View File

@ -1,238 +0,0 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import kotlinx.android.synthetic.main.catalogue_main_controller.view.*
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* This controller shows and manages the different catalogues enabled by the user.
* This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter]
* [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
* [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click.
* [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click
*/
class CatalogueMainController : NucleusController<CatalogueMainPresenter>(),
SourceLoginDialog.Listener,
FlexibleAdapter.OnItemClickListener,
CatalogueMainAdapter.OnBrowseClickListener,
CatalogueMainAdapter.OnLatestClickListener {
/**
* Application preferences.
*/
private val preferences: PreferencesHelper = Injekt.get()
/**
* Adapter containing sources.
*/
private var adapter : CatalogueMainAdapter? = null
/**
* Called when controller is initialized.
*/
init {
// Enable the option menu
setHasOptionsMenu(true)
}
/**
* Set the title of controller.
*
* @return title.
*/
override fun getTitle(): String? {
return applicationContext?.getString(R.string.label_catalogues)
}
/**
* Create the [CatalogueMainPresenter] used in controller.
*
* @return instance of [CatalogueMainPresenter]
*/
override fun createPresenter(): CatalogueMainPresenter {
return CatalogueMainPresenter()
}
/**
* Initiate the view with [R.layout.catalogue_main_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_main_controller, container, false)
}
/**
* Called when the view is created
*
* @param view view of controller
* @param savedViewState information from previous state.
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
adapter = CatalogueMainAdapter(this)
with(view) {
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(context))
}
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
presenter.updateSources()
}
}
/**
* Called when login dialog is closed, refreshes the adapter.
*
* @param source clicked item containing source information.
*/
override fun loginDialogClosed(source: LoginSource) {
if (source.isLogged()) {
adapter?.clear()
presenter.loadSources()
}
}
/**
* Called when item is clicked
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
val source = item.source
if (source is LoginSource && !source.isLogged()) {
val dialog = SourceLoginDialog(source)
dialog.targetController = this
dialog.showDialog(router)
} else {
// Open the catalogue view.
openCatalogue(source, CatalogueController(source))
}
return false
}
/**
* Called when browse is clicked in [CatalogueMainAdapter]
*/
override fun onBrowseClick(position: Int) {
onItemClick(position)
}
/**
* Called when latest is clicked in [CatalogueMainAdapter]
*/
override fun onLatestClick(position: Int) {
val item = adapter?.getItem(position) as? SourceItem ?: return
openCatalogue(item.source, LatestUpdatesController(item.source))
}
/**
* Opens a catalogue with the given controller.
*/
private fun openCatalogue(source: CatalogueSource, controller: CatalogueController) {
preferences.lastUsedCatalogueSource().set(source.id)
router.pushController(RouterTransaction.with(controller)
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.catalogue_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextChangeEvents()
.filter { it.isSubmitted }
.subscribeUntilDestroy {
val query = it.queryText().toString()
router.pushController((RouterTransaction.with(CatalogueSearchController(query)))
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
}
/**
* Called when an option menu item has been selected by the user.
*
* @param item The selected item.
* @return True if this event has been consumed, false if it has not.
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// Initialize option to open catalogue settings.
R.id.action_settings -> {
router.pushController((RouterTransaction.with(SettingsSourcesController()))
.popChangeHandler(SettingsSourcesFadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Called to update adapter containing sources.
*/
fun setSources(sources: List<IFlexible<*>>) {
adapter?.updateDataSet(sources)
}
/**
* Called to set the last used catalogue at the top of the view.
*/
fun setLastUsedSource(item: SourceItem?) {
adapter?.removeAllScrollableHeaders()
if (item != null) {
adapter?.addScrollableHeader(item)
}
}
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
}

View File

@ -1,104 +0,0 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Bundle
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Presenter of [CatalogueMainController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param sourceManager manages the different sources.
* @param preferences application preferences.
*/
class CatalogueMainPresenter(
val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<CatalogueMainController>() {
/**
* Enabled sources.
*/
var sources = getEnabledSources()
/**
* Subscription for retrieving enabled sources.
*/
private var sourceSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
// Load enabled and last used sources
loadSources()
loadLastUsedSource()
}
/**
* Unsubscribe and create a new subscription to fetch enabled sources.
*/
fun loadSources() {
sourceSubscription?.unsubscribe()
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
// Catalogues without a lang defined will be placed at the end
when {
d1 == "" && d2 != "" -> 1
d2 == "" && d1 != "" -> -1
else -> d1.compareTo(d2)
}
}
val byLang = sources.groupByTo(map, { it.lang })
val sourceItems = byLang.flatMap {
val langItem = LangItem(it.key)
it.value.map { source -> SourceItem(source, langItem) }
}
sourceSubscription = Observable.just(sourceItems)
.subscribeLatestCache(CatalogueMainController::setSources)
}
private fun loadLastUsedSource() {
val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
// Emit the first item immediately but delay subsequent emissions by 500ms.
Observable.merge(
sharedObs.take(1),
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
.distinctUntilChanged()
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
.subscribeLatestCache(CatalogueMainController::setLastUsedSource)
}
fun updateSources() {
sources = getEnabledSources()
loadSources()
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<CatalogueSource> {
val languages = preferences.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" } +
sourceManager.get(LocalSource.ID) as LocalSource
}
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.category package eu.kanade.tachiyomi.ui.category
import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
@ -15,7 +14,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.categories_controller.view.* import kotlinx.android.synthetic.main.categories_controller.*
/** /**
* Controller to manage the categories for the users' library. * Controller to manage the categories for the users' library.
@ -70,22 +69,19 @@ class CategoryController : NucleusController<CategoryPresenter>(),
* Called after view inflation. Used to initialize the view. * Called after view inflation. Used to initialize the view.
* *
* @param view The view of this controller. * @param view The view of this controller.
* @param savedViewState The saved state of the view.
*/ */
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
with(view) { adapter = CategoryAdapter(this@CategoryController)
adapter = CategoryAdapter(this@CategoryController) recycler.layoutManager = LinearLayoutManager(view.context)
recycler.layoutManager = LinearLayoutManager(context) recycler.setHasFixedSize(true)
recycler.setHasFixedSize(true) recycler.adapter = adapter
recycler.adapter = adapter adapter?.isHandleDragEnabled = true
adapter?.isHandleDragEnabled = true adapter?.isPermanentDelete = false
adapter?.isPermanentDelete = false
fab.clicks().subscribeUntilDestroy { fab.clicks().subscribeUntilDestroy {
CategoryCreateDialog(this@CategoryController).showDialog(router, null) CategoryCreateDialog(this@CategoryController).showDialog(router, null)
}
} }
} }
@ -95,12 +91,12 @@ class CategoryController : NucleusController<CategoryPresenter>(),
* @param view The view of this controller. * @param view The view of this controller.
*/ */
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
// Manually call callback to delete categories if required // Manually call callback to delete categories if required
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
undoHelper = null undoHelper = null
actionMode = null actionMode = null
adapter = null adapter = null
super.onDestroyView(view)
} }
/** /**
@ -111,9 +107,14 @@ class CategoryController : NucleusController<CategoryPresenter>(),
fun setCategories(categories: List<CategoryItem>) { fun setCategories(categories: List<CategoryItem>) {
actionMode?.finish() actionMode?.finish()
adapter?.updateDataSet(categories) adapter?.updateDataSet(categories)
val selected = categories.filter { it.isSelected } if (categories.isNotEmpty()) {
if (selected.isNotEmpty()) { empty_view.hide()
selected.forEach { onItemLongClick(categories.indexOf(it)) } val selected = categories.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(categories.indexOf(it)) }
}
} else {
empty_view.show(R.drawable.ic_shape_black_128dp, R.string.information_empty_category)
} }
} }

View File

@ -1,14 +1,10 @@
package eu.kanade.tachiyomi.ui.category package eu.kanade.tachiyomi.ui.category
import android.graphics.Color
import android.graphics.Typeface
import android.view.View import android.view.View
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getRound import eu.kanade.tachiyomi.util.getRound
import kotlinx.android.synthetic.main.categories_item.view.* import kotlinx.android.synthetic.main.categories_item.*
/** /**
* Holder used to display category items. * Holder used to display category items.
@ -16,16 +12,16 @@ import kotlinx.android.synthetic.main.categories_item.view.*
* @param view The view used by category items. * @param view The view used by category items.
* @param adapter The adapter containing this holder. * @param adapter The adapter containing this holder.
*/ */
class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) { class CategoryHolder(view: View, val adapter: CategoryAdapter) : BaseFlexibleViewHolder(view, adapter) {
init { init {
// Create round letter image onclick to simulate long click // Create round letter image onclick to simulate long click
itemView.image.setOnClickListener { image.setOnClickListener {
// Simulate long click on this view to enter selection mode // Simulate long click on this view to enter selection mode
onLongClick(view) onLongClick(view)
} }
setDragHandleView(itemView.reorder) setDragHandleView(reorder)
} }
/** /**
@ -35,11 +31,11 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
*/ */
fun bind(category: Category) { fun bind(category: Category) {
// Set capitalized title. // Set capitalized title.
itemView.title.text = category.name.capitalize() title.text = category.name.capitalize()
// Update circle letter image. // Update circle letter image.
itemView.post { itemView.post {
itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false)) image.setImageDrawable(image.getRound(category.name.take(1).toUpperCase(),false))
} }
} }

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.download package eu.kanade.tachiyomi.ui.download
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.view.* import android.view.*
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -8,7 +7,7 @@ import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import kotlinx.android.synthetic.main.download_controller.view.* import kotlinx.android.synthetic.main.download_controller.*
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -52,21 +51,19 @@ class DownloadController : NucleusController<DownloadPresenter>() {
return resources?.getString(R.string.label_download_queue) return resources?.getString(R.string.label_download_queue)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
// Check if download queue is empty and update information accordingly. // Check if download queue is empty and update information accordingly.
setInformationView() setInformationView()
// Initialize adapter. // Initialize adapter.
adapter = DownloadAdapter() adapter = DownloadAdapter()
with(view) { recycler.adapter = adapter
recycler.adapter = adapter
// Set the layout manager for the recycler and fixed size. // Set the layout manager for the recycler and fixed size.
recycler.layoutManager = LinearLayoutManager(context) recycler.layoutManager = LinearLayoutManager(view.context)
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
}
// Suscribe to changes // Suscribe to changes
DownloadService.runningRelay DownloadService.runningRelay
@ -83,12 +80,12 @@ class DownloadController : NucleusController<DownloadPresenter>() {
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
for (subscription in progressSubscriptions.values) { for (subscription in progressSubscriptions.values) {
subscription.unsubscribe() subscription.unsubscribe()
} }
progressSubscriptions.clear() progressSubscriptions.clear()
adapter = null adapter = null
super.onDestroyView(view)
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -232,20 +229,18 @@ class DownloadController : NucleusController<DownloadPresenter>() {
* @return the holder of the download or null if it's not bound. * @return the holder of the download or null if it's not bound.
*/ */
private fun getHolder(download: Download): DownloadHolder? { private fun getHolder(download: Download): DownloadHolder? {
val recycler = view?.recycler ?: return null return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
} }
/** /**
* Set information view when queue is empty * Set information view when queue is empty
*/ */
private fun setInformationView() { private fun setInformationView() {
val emptyView = view?.empty_view ?: return
if (presenter.downloadQueue.isEmpty()) { if (presenter.downloadQueue.isEmpty()) {
emptyView.show(R.drawable.ic_file_download_black_128dp, empty_view?.show(R.drawable.ic_file_download_black_128dp,
R.string.information_no_downloads) R.string.information_no_downloads)
} else { } else {
emptyView.hide() empty_view?.hide()
} }
} }

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.download package eu.kanade.tachiyomi.ui.download
import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import kotlinx.android.synthetic.main.download_item.view.* import kotlinx.android.synthetic.main.download_item.view.*
/** /**
@ -12,7 +12,7 @@ import kotlinx.android.synthetic.main.download_item.view.*
* @param view the inflated view for this holder. * @param view the inflated view for this holder.
* @constructor creates a new download holder. * @constructor creates a new download holder.
*/ */
class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) { class DownloadHolder(private val view: View) : BaseViewHolder(view) {
private lateinit var download: Download private lateinit var download: Download

View File

@ -1,16 +0,0 @@
package eu.kanade.tachiyomi.ui.latest_updates
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.Pager
/**
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
*/
class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source)
}
}

View File

@ -26,6 +26,8 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
} }
} }
private var boundViews = arrayListOf<View>()
/** /**
* Creates a new view for this adapter. * Creates a new view for this adapter.
* *
@ -45,6 +47,7 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
*/ */
override fun bindView(view: View, position: Int) { override fun bindView(view: View, position: Int) {
(view as LibraryCategoryView).onBind(categories[position]) (view as LibraryCategoryView).onBind(categories[position])
boundViews.add(view)
} }
/** /**
@ -55,6 +58,7 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
*/ */
override fun recycleView(view: View, position: Int) { override fun recycleView(view: View, position: Int) {
(view as LibraryCategoryView).onRecycle() (view as LibraryCategoryView).onRecycle()
boundViews.remove(view)
} }
/** /**
@ -79,10 +83,21 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
/** /**
* Returns the position of the view. * Returns the position of the view.
*/ */
override fun getItemPosition(obj: Any?): Int { override fun getItemPosition(obj: Any): Int {
val view = obj as? LibraryCategoryView ?: return POSITION_NONE val view = obj as? LibraryCategoryView ?: return POSITION_NONE
val index = categories.indexOfFirst { it.id == view.category.id } val index = categories.indexOfFirst { it.id == view.category.id }
return if (index == -1) POSITION_NONE else index return if (index == -1) POSITION_NONE else index
} }
/**
* Called when the view of this adapter is being destroyed.
*/
fun onDestroy() {
for (view in boundViews) {
if (view is LibraryCategoryView) {
view.unsubscribe()
}
}
}
} }

View File

@ -124,12 +124,11 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
fun onRecycle() { fun onRecycle() {
adapter.setItems(emptyList()) adapter.setItems(emptyList())
adapter.clearSelection() adapter.clearSelection()
subscriptions.clear() unsubscribe()
} }
override fun onDetachedFromWindow() { fun unsubscribe() {
subscriptions.clear() subscriptions.clear()
super.onDetachedFromWindow()
} }
/** /**

View File

@ -14,8 +14,6 @@ import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v4.view.pageSelections import com.jakewharton.rxbinding.support.v4.view.pageSelections
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
@ -30,13 +28,14 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.category.CategoryController import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.library_controller.view.* import kotlinx.android.synthetic.main.library_controller.*
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import rx.Subscription import rx.Subscription
import timber.log.Timber import timber.log.Timber
@ -100,14 +99,8 @@ class LibraryController(
private set private set
/** /**
* TabLayout of the categories. * Adapter of the view pager.
*/ */
private val tabs: TabLayout?
get() = activity?.tabs
private val drawer: DrawerLayout?
get() = activity?.drawer
private var adapter: LibraryAdapter? = null private var adapter: LibraryAdapter? = null
/** /**
@ -126,6 +119,7 @@ class LibraryController(
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
retainViewMode = RetainViewMode.RETAIN_DETACH
} }
override fun getTitle(): String? { override fun getTitle(): String? {
@ -140,47 +134,42 @@ class LibraryController(
return inflater.inflate(R.layout.library_controller, container, false) return inflater.inflate(R.layout.library_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
adapter = LibraryAdapter(this) adapter = LibraryAdapter(this)
with(view) { library_pager.adapter = adapter
view_pager.adapter = adapter library_pager.pageSelections().skip(1).subscribeUntilDestroy {
view_pager.pageSelections().skip(1).subscribeUntilDestroy { preferences.lastUsedCategory().set(it)
preferences.lastUsedCategory().set(it) activeCategory = it
activeCategory = it }
}
getColumnsPreferenceForCurrentOrientation().asObservable() getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { mangaPerRow = it } .doOnNext { mangaPerRow = it }
.skip(1) .skip(1)
// Set again the adapter to recalculate the covers height // Set again the adapter to recalculate the covers height
.subscribeUntilDestroy { reattachAdapter() } .subscribeUntilDestroy { reattachAdapter() }
if (selectedMangas.isNotEmpty()) { if (selectedMangas.isNotEmpty()) {
createActionModeIfNeeded() createActionModeIfNeeded()
}
} }
} }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type) super.onChangeStarted(handler, type)
if (type.isEnter) { if (type.isEnter) {
activity?.tabs?.setupWithViewPager(view?.view_pager) activity?.tabs?.setupWithViewPager(library_pager)
presenter.subscribeLibrary()
} }
} }
override fun onAttach(view: View) {
super.onAttach(view)
presenter.subscribeLibrary()
}
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view) adapter?.onDestroy()
adapter = null adapter = null
actionMode = null actionMode = null
tabsVisibilitySubscription?.unsubscribe() tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null tabsVisibilitySubscription = null
super.onDestroyView(view)
} }
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup { override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
@ -236,14 +225,14 @@ class LibraryController(
// Show empty view if needed // Show empty view if needed
if (mangaMap.isNotEmpty()) { if (mangaMap.isNotEmpty()) {
view.empty_view.hide() empty_view.hide()
} else { } else {
view.empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library) empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library)
} }
// Get the current active category. // Get the current active category.
val activeCat = if (adapter.categories.isNotEmpty()) val activeCat = if (adapter.categories.isNotEmpty())
view.view_pager.currentItem library_pager.currentItem
else else
activeCategory activeCategory
@ -251,14 +240,14 @@ class LibraryController(
adapter.categories = categories adapter.categories = categories
// Restore active category. // Restore active category.
view.view_pager.setCurrentItem(activeCat, false) library_pager.setCurrentItem(activeCat, false)
tabsVisibilityRelay.call(categories.size > 1) tabsVisibilityRelay.call(categories.size > 1)
// Delay the scroll position to allow the view to be properly measured. // Delay the scroll position to allow the view to be properly measured.
view.post { view.post {
if (isAttached) { if (isAttached) {
tabs?.setScrollPosition(view.view_pager.currentItem, 0f, true) activity?.tabs?.setScrollPosition(library_pager.currentItem, 0f, true)
} }
} }
@ -301,14 +290,13 @@ class LibraryController(
* Reattaches the adapter to the view pager to recreate fragments * Reattaches the adapter to the view pager to recreate fragments
*/ */
private fun reattachAdapter() { private fun reattachAdapter() {
val pager = view?.view_pager ?: return
val adapter = adapter ?: return val adapter = adapter ?: return
val position = pager.currentItem val position = library_pager.currentItem
adapter.recycle = false adapter.recycle = false
pager.adapter = adapter library_pager.adapter = adapter
pager.currentItem = position library_pager.currentItem = position
adapter.recycle = true adapter.recycle = true
} }
@ -334,7 +322,7 @@ class LibraryController(
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
if (!query.isNullOrEmpty()) { if (!query.isEmpty()) {
searchItem.expandActionView() searchItem.expandActionView()
searchView.setQuery(query, true) searchView.setQuery(query, true)
searchView.clearFocus() searchView.clearFocus()
@ -364,15 +352,13 @@ class LibraryController(
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_filter -> { R.id.action_filter -> {
navView?.let { drawer?.openDrawer(Gravity.END) } navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
} }
R.id.action_update_library -> { R.id.action_update_library -> {
activity?.let { LibraryUpdateService.start(it) } activity?.let { LibraryUpdateService.start(it) }
} }
R.id.action_edit_categories -> { R.id.action_edit_categories -> {
router.pushController(RouterTransaction.with(CategoryController()) router.pushController(CategoryController().withFadeTransaction())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
@ -428,9 +414,7 @@ class LibraryController(
// Notify the presenter a manga is being opened. // Notify the presenter a manga is being opened.
presenter.onOpenManga() presenter.onOpenManga()
router.pushController(RouterTransaction.with(MangaController(manga)) router.pushController(MangaController(manga).withFadeTransaction())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
/** /**
@ -465,11 +449,11 @@ class LibraryController(
.toTypedArray() .toTypedArray()
ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes) ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes)
.showDialog(router, null) .showDialog(router)
} }
private fun showDeleteMangaDialog() { private fun showDeleteMangaDialog() {
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router, null) DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
} }
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) { override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
@ -484,8 +468,6 @@ class LibraryController(
/** /**
* Changes the cover for the selected manga. * Changes the cover for the selected manga.
*
* @param mangas a list of selected manga.
*/ */
private fun changeSelectedCover() { private fun changeSelectedCover() {
val manga = selectedMangas.firstOrNull() ?: return val manga = selectedMangas.firstOrNull() ?: return

View File

@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.catalogue_grid_item.*
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -30,30 +30,28 @@ class LibraryGridHolder(
*/ */
override fun onSetValues(item: LibraryItem) { override fun onSetValues(item: LibraryItem) {
// Update the title of the manga. // Update the title of the manga.
view.title.text = item.manga.title title.text = item.manga.title
// Update the unread count and its visibility. // Update the unread count and its visibility.
with(view.unread_text) { with(unread_text) {
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
text = item.manga.unread.toString() text = item.manga.unread.toString()
} }
// Update the download count and its visibility. // Update the download count and its visibility.
with(view.download_text) { with(download_text) {
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
text = item.downloadCount.toString() text = item.downloadCount.toString()
} }
//set local visibility if its local manga //set local visibility if its local manga
with(view.local_text) { local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
// Update the cover. // Update the cover.
GlideApp.with(view.context).clear(view.thumbnail) GlideApp.with(view.context).clear(thumbnail)
GlideApp.with(view.context) GlideApp.with(view.context)
.load(item.manga) .load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(view.thumbnail) .into(thumbnail)
} }
} }

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
/** /**
* Generic class used to hold the displayed data of a manga in the library. * Generic class used to hold the displayed data of a manga in the library.
@ -14,7 +14,7 @@ import eu.davidea.viewholders.FlexibleViewHolder
abstract class LibraryHolder( abstract class LibraryHolder(
view: View, view: View,
adapter: FlexibleAdapter<*> adapter: FlexibleAdapter<*>
) : FlexibleViewHolder(view, adapter) { ) : BaseFlexibleViewHolder(view, adapter) {
/** /**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this

View File

@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import kotlinx.android.synthetic.main.catalogue_list_item.view.* import kotlinx.android.synthetic.main.catalogue_list_item.*
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -30,38 +30,36 @@ class LibraryListHolder(
*/ */
override fun onSetValues(item: LibraryItem) { override fun onSetValues(item: LibraryItem) {
// Update the title of the manga. // Update the title of the manga.
itemView.title.text = item.manga.title title.text = item.manga.title
// Update the unread count and its visibility. // Update the unread count and its visibility.
with(itemView.unread_text) { with(unread_text) {
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
text = item.manga.unread.toString() text = item.manga.unread.toString()
} }
// Update the download count and its visibility. // Update the download count and its visibility.
with(itemView.download_text) { with(download_text) {
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
text = "${item.downloadCount}" text = "${item.downloadCount}"
} }
//show local text badge if local manga //show local text badge if local manga
with(itemView.local_text) { local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
// Create thumbnail onclick to simulate long click // Create thumbnail onclick to simulate long click
itemView.thumbnail.setOnClickListener { thumbnail.setOnClickListener {
// Simulate long click on this view to enter selection mode // Simulate long click on this view to enter selection mode
onLongClick(itemView) onLongClick(itemView)
} }
// Update the cover. // Update the cover.
GlideApp.with(itemView.context).clear(itemView.thumbnail) GlideApp.with(itemView.context).clear(thumbnail)
GlideApp.with(itemView.context) GlideApp.with(itemView.context)
.load(item.manga) .load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.circleCrop() .circleCrop()
.dontAnimate() .dontAnimate()
.into(itemView.thumbnail) .into(thumbnail)
} }
} }

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.os.Bundle import android.os.Bundle
import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -107,12 +106,6 @@ class LibraryPresenter(
* @param map the map to filter. * @param map the map to filter.
*/ */
private fun applyFilters(map: LibraryMap): LibraryMap { private fun applyFilters(map: LibraryMap): LibraryMap {
// Cached list of downloaded manga directories given a source id.
val mangaDirsForSource = mutableMapOf<Long, Map<String?, UniFile>>()
// Cached list of downloaded chapter directories for a manga.
val chapterDirectories = mutableMapOf<Long, Boolean>()
val filterDownloaded = preferences.filterDownloaded().getOrDefault() val filterDownloaded = preferences.filterDownloaded().getOrDefault()
val filterUnread = preferences.filterUnread().getOrDefault() val filterUnread = preferences.filterUnread().getOrDefault()
@ -121,7 +114,7 @@ class LibraryPresenter(
val filterFn: (LibraryItem) -> Boolean = f@ { item -> val filterFn: (LibraryItem) -> Boolean = f@ { item ->
// Filter out manga without source. // Filter out manga without source.
val source = sourceManager.get(item.manga.source) ?: return@f false sourceManager.get(item.manga.source) ?: return@f false
// Filter when there isn't unread chapters. // Filter when there isn't unread chapters.
if (filterUnread && item.manga.unread == 0) { if (filterUnread && item.manga.unread == 0) {
@ -132,28 +125,14 @@ class LibraryPresenter(
return@f false return@f false
} }
// Filter when the download directory doesn't exist or is null. // Filter when there are no downloads.
if (filterDownloaded) { if (filterDownloaded) {
// Don't bother with directory checking if download count has been set. // Don't bother with directory checking if download count has been set.
if (item.downloadCount != -1) { if (item.downloadCount != -1) {
return@f item.downloadCount > 0 return@f item.downloadCount > 0
} }
// Get the directories for the source of the manga. return@f downloadManager.getDownloadCount(item.manga) > 0
val dirsForSource = mangaDirsForSource.getOrPut(source.id) {
val sourceDir = downloadManager.findSourceDir(source)
sourceDir?.listFiles()?.associateBy { it.name }.orEmpty()
}
val mangaDirName = downloadManager.getMangaDirName(item.manga)
val mangaDir = dirsForSource[mangaDirName] ?: return@f false
val hasDirs = chapterDirectories.getOrPut(item.manga.id!!) {
mangaDir.listFiles()?.isNotEmpty() ?: false
}
if (!hasDirs) {
return@f false
}
} }
true true
} }
@ -177,31 +156,9 @@ class LibraryPresenter(
return return
} }
// Cached list of downloaded manga directories given a source id.
val mangaDirsForSource = mutableMapOf<Long, Map<String?, UniFile>>()
// Cached list of downloaded chapter directories for a manga.
val chapterDirectories = mutableMapOf<Long, Int>()
val downloadCountFn: (LibraryItem) -> Int = f@ { item ->
val source = sourceManager.get(item.manga.source) ?: return@f 0
// Get the directories for the source of the manga.
val dirsForSource = mangaDirsForSource.getOrPut(source.id) {
val sourceDir = downloadManager.findSourceDir(source)
sourceDir?.listFiles()?.associateBy { it.name }.orEmpty()
}
val mangaDirName = downloadManager.getMangaDirName(item.manga)
val mangaDir = dirsForSource[mangaDirName] ?: return@f 0
chapterDirectories.getOrPut(item.manga.id!!) {
mangaDir.listFiles()?.size ?: 0
}
}
for ((_, itemList) in map) { for ((_, itemList) in map) {
for (item in itemList) { for (item in itemList) {
item.downloadCount = downloadCountFn(item) item.downloadCount = downloadManager.getDownloadCount(item.manga)
} }
} }
} }
@ -360,7 +317,7 @@ class LibraryPresenter(
if (deleteChapters) { if (deleteChapters) {
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
if (source != null) { if (source != null) {
downloadManager.findMangaDir(source, manga)?.delete() downloadManager.deleteManga(manga, source)
} }
} }
} }

View File

@ -9,16 +9,12 @@ import android.support.v4.widget.DrawerLayout
import android.support.v7.graphics.drawable.DrawerArrowDrawable import android.support.v7.graphics.drawable.DrawerArrowDrawable
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.* import com.bluelinelabs.conductor.*
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.*
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController
import eu.kanade.tachiyomi.ui.download.DownloadController import eu.kanade.tachiyomi.ui.download.DownloadController
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
@ -83,16 +79,13 @@ class MainActivity : BaseActivity() {
R.id.nav_drawer_library -> setRoot(LibraryController(), id) R.id.nav_drawer_library -> setRoot(LibraryController(), id)
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id) R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id) R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
R.id.nav_drawer_downloads -> { R.id.nav_drawer_downloads -> {
router.pushController(RouterTransaction.with(DownloadController()) router.pushController(DownloadController().withFadeTransaction())
.pushChangeHandler(FadeChangeHandler()) }
.popChangeHandler(FadeChangeHandler())) R.id.nav_drawer_settings -> {
router.pushController(SettingsMainController().withFadeTransaction())
} }
R.id.nav_drawer_settings ->
router.pushController(RouterTransaction.with(SettingsMainController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
} }
drawer.closeDrawer(GravityCompat.START) drawer.closeDrawer(GravityCompat.START)
@ -189,10 +182,7 @@ class MainActivity : BaseActivity() {
} }
private fun setRoot(controller: Controller, id: Int) { private fun setRoot(controller: Controller, id: Int) {
router.setRoot(RouterTransaction.with(controller) router.setRoot(controller.withFadeTransaction().tag(id.toString()))
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler())
.tag(id.toString()))
} }
private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) { private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) {

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.manga package eu.kanade.tachiyomi.ui.manga
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.TabLayout import android.support.design.widget.TabLayout
import android.support.graphics.drawable.VectorDrawableCompat import android.support.graphics.drawable.VectorDrawableCompat
@ -32,7 +30,7 @@ import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
import eu.kanade.tachiyomi.ui.manga.track.TrackController import eu.kanade.tachiyomi.ui.manga.track.TrackController
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.manga_controller.view.* import kotlinx.android.synthetic.main.manga_controller.*
import rx.Subscription import rx.Subscription
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -81,21 +79,19 @@ class MangaController : RxController, TabbedController {
return inflater.inflate(R.layout.manga_controller, container, false) return inflater.inflate(R.layout.manga_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
if (manga == null || source == null) return if (manga == null || source == null) return
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
with(view) { adapter = MangaDetailAdapter()
adapter = MangaDetailAdapter() manga_pager.offscreenPageLimit = 3
view_pager.offscreenPageLimit = 3 manga_pager.adapter = adapter
view_pager.adapter = adapter
if (!fromCatalogue) if (!fromCatalogue)
view_pager.currentItem = CHAPTERS_CONTROLLER manga_pager.currentItem = CHAPTERS_CONTROLLER
}
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
@ -106,7 +102,7 @@ class MangaController : RxController, TabbedController {
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type) super.onChangeStarted(handler, type)
if (type.isEnter) { if (type.isEnter) {
activity?.tabs?.setupWithViewPager(view?.view_pager) activity?.tabs?.setupWithViewPager(manga_pager)
trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
} }
} }

View File

@ -2,41 +2,41 @@ package eu.kanade.tachiyomi.ui.manga.chapter
import android.view.View import android.view.View
import android.widget.PopupMenu import android.widget.PopupMenu
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat import eu.kanade.tachiyomi.util.setVectorCompat
import kotlinx.android.synthetic.main.chapters_item.view.* import kotlinx.android.synthetic.main.chapters_item.*
import java.util.* import java.util.*
class ChapterHolder( class ChapterHolder(
private val view: View, private val view: View,
private val adapter: ChaptersAdapter private val adapter: ChaptersAdapter
) : FlexibleViewHolder(view, adapter) { ) : BaseFlexibleViewHolder(view, adapter) {
init { init {
// We need to post a Runnable to show the popup to make sure that the PopupMenu is // We need to post a Runnable to show the popup to make sure that the PopupMenu is
// correctly positioned. The reason being that the view may change position before the // correctly positioned. The reason being that the view may change position before the
// PopupMenu is shown. // PopupMenu is shown.
view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
} }
fun bind(item: ChapterItem, manga: Manga) = with(view) { fun bind(item: ChapterItem, manga: Manga) {
val chapter = item.chapter val chapter = item.chapter
chapter_title.text = when (manga.displayMode) { chapter_title.text = when (manga.displayMode) {
Manga.DISPLAY_NUMBER -> { Manga.DISPLAY_NUMBER -> {
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
context.getString(R.string.display_mode_chapter, number) itemView.context.getString(R.string.display_mode_chapter, number)
} }
else -> chapter.name else -> chapter.name
} }
// Set the correct drawable for dropdown and update the tint to match theme. // Set the correct drawable for dropdown and update the tint to match theme.
view.chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color)) chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
// Set correct text color // Set correct text color
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
@ -53,14 +53,14 @@ class ChapterHolder(
chapter_scanlator.text = chapter.scanlator chapter_scanlator.text = chapter.scanlator
//allow longer titles if there is no scanlator (most sources) //allow longer titles if there is no scanlator (most sources)
if (chapter_scanlator.text.isNullOrBlank()) { if (chapter_scanlator.text.isNullOrBlank()) {
chapter_title.setMaxLines(2) chapter_title.maxLines = 2
chapter_scanlator.gone() chapter_scanlator.gone()
} else { } else {
chapter_title.setMaxLines(1) chapter_title.maxLines = 1
} }
chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) { chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) {
context.getString(R.string.chapter_progress, chapter.last_page_read + 1) itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
} else { } else {
"" ""
} }
@ -68,7 +68,7 @@ class ChapterHolder(
notifyStatus(item.status) notifyStatus(item.status)
} }
fun notifyStatus(status: Int) = with(view.download_text) { fun notifyStatus(status: Int) = with(download_text) {
when (status) { when (status) {
Download.QUEUE -> setText(R.string.chapter_queued) Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> setText(R.string.chapter_downloading) Download.DOWNLOADING -> setText(R.string.chapter_downloading)

View File

@ -4,7 +4,6 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
@ -26,7 +25,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.getCoordinates import eu.kanade.tachiyomi.util.getCoordinates
import eu.kanade.tachiyomi.util.snack import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.chapters_controller.view.* import kotlinx.android.synthetic.main.chapters_controller.*
import timber.log.Timber import timber.log.Timber
class ChaptersController : NucleusController<ChaptersPresenter>(), class ChaptersController : NucleusController<ChaptersPresenter>(),
@ -69,57 +68,55 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
return inflater.inflate(R.layout.chapters_controller, container, false) return inflater.inflate(R.layout.chapters_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
// Init RecyclerView and adapter // Init RecyclerView and adapter
adapter = ChaptersAdapter(this, view.context) adapter = ChaptersAdapter(this, view.context)
with(view) { recycler.adapter = adapter
recycler.adapter = adapter recycler.layoutManager = LinearLayoutManager(view.context)
recycler.layoutManager = LinearLayoutManager(context) recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) recycler.setHasFixedSize(true)
recycler.setHasFixedSize(true) adapter?.fastScroller = fast_scroller
adapter?.fastScroller = view.fast_scroller
swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() } swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
fab.clicks().subscribeUntilDestroy { fab.clicks().subscribeUntilDestroy {
val item = presenter.getNextUnreadChapter() val item = presenter.getNextUnreadChapter()
if (item != null) { if (item != null) {
// Create animation listener // Create animation listener
val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) { override fun onAnimationStart(animation: Animator?) {
openChapter(item.chapter, true) openChapter(item.chapter, true)
}
} }
// Get coordinates and start animation
val coordinates = fab.getCoordinates()
if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
openChapter(item.chapter)
}
} else {
context.toast(R.string.no_next_chapter)
} }
// Get coordinates and start animation
val coordinates = fab.getCoordinates()
if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
openChapter(item.chapter)
}
} else {
view.context.toast(R.string.no_next_chapter)
} }
} }
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null adapter = null
actionMode = null actionMode = null
super.onDestroyView(view)
} }
override fun onActivityResumed(activity: Activity) { override fun onActivityResumed(activity: Activity) {
val view = view ?: return if (view == null) return
// Check if animation view is visible // Check if animation view is visible
if (view.reveal_view.visibility == View.VISIBLE) { if (reveal_view.visibility == View.VISIBLE) {
// Show the unReveal effect // Show the unReveal effect
val coordinates = view.fab.getCoordinates() val coordinates = fab.getCoordinates()
view.reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
} }
super.onActivityResumed(activity) super.onActivityResumed(activity)
} }
@ -213,16 +210,16 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
} }
fun fetchChaptersFromSource() { fun fetchChaptersFromSource() {
view?.swipe_refresh?.isRefreshing = true swipe_refresh?.isRefreshing = true
presenter.fetchChaptersFromSource() presenter.fetchChaptersFromSource()
} }
fun onFetchChaptersDone() { fun onFetchChaptersDone() {
view?.swipe_refresh?.isRefreshing = false swipe_refresh?.isRefreshing = false
} }
fun onFetchChaptersError(error: Throwable) { fun onFetchChaptersError(error: Throwable) {
view?.swipe_refresh?.isRefreshing = false swipe_refresh?.isRefreshing = false
activity?.toast(error.message) activity?.toast(error.message)
} }
@ -231,7 +228,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
} }
private fun getHolder(chapter: Chapter): ChapterHolder? { private fun getHolder(chapter: Chapter): ChapterHolder? {
return view?.recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
} }
fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
@ -365,7 +362,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
presenter.downloadChapters(chapters) presenter.downloadChapters(chapters)
if (view != null && !presenter.manga.favorite) { if (view != null && !presenter.manga.favorite) {
view.recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_add) { setAction(R.string.action_add) {
presenter.addToLibrary() presenter.addToLibrary()
} }

View File

@ -128,13 +128,11 @@ class ChaptersPresenter(
* @param chapters the list of chapter from the database. * @param chapters the list of chapter from the database.
*/ */
private fun setDownloadedChapters(chapters: List<ChapterItem>) { private fun setDownloadedChapters(chapters: List<ChapterItem>) {
val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return for (chapter in chapters) {
val cached = mutableMapOf<Chapter, String>() if (downloadManager.isChapterDownloaded(chapter, manga)) {
files.mapNotNull { it.name } chapter.status = Download.DOWNLOADED
.mapNotNull { name -> chapters.find { }
name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) } }
} }
.forEach { it.status = Download.DOWNLOADED }
} }
/** /**
@ -283,7 +281,7 @@ class ChaptersPresenter(
*/ */
private fun deleteChapter(chapter: ChapterItem) { private fun deleteChapter(chapter: ChapterItem) {
downloadManager.queue.remove(chapter) downloadManager.queue.remove(chapter)
downloadManager.deleteChapter(source, manga, chapter) downloadManager.deleteChapter(chapter, manga, source)
chapter.status = Download.NOT_DOWNLOADED chapter.status = Download.NOT_DOWNLOADED
chapter.download = null chapter.download = null
} }

View File

@ -39,7 +39,7 @@ import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import jp.wasabeef.glide.transformations.CropSquareTransformation import jp.wasabeef.glide.transformations.CropSquareTransformation
import jp.wasabeef.glide.transformations.MaskTransformation import jp.wasabeef.glide.transformations.MaskTransformation
import kotlinx.android.synthetic.main.manga_info_controller.view.* import kotlinx.android.synthetic.main.manga_info_controller.*
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat import java.text.DecimalFormat
@ -71,17 +71,14 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
return inflater.inflate(R.layout.manga_info_controller, container, false) return inflater.inflate(R.layout.manga_info_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
with(view) { // Set onclickListener to toggle favorite when FAB clicked.
// Set onclickListener to toggle favorite when FAB clicked. fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
// Set SwipeRefresh to refresh manga data.
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
}
// Set SwipeRefresh to refresh manga data.
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -124,50 +121,49 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
*/ */
private fun setMangaInfo(manga: Manga, source: Source?) { private fun setMangaInfo(manga: Manga, source: Source?) {
val view = view ?: return val view = view ?: return
with(view) {
// Update artist TextView.
manga_artist.text = manga.artist
// Update author TextView. // Update artist TextView.
manga_author.text = manga.author manga_artist.text = manga.artist
// If manga source is known update source TextView. // Update author TextView.
if (source != null) { manga_author.text = manga.author
manga_source.text = source.toString()
}
// Update genres TextView. // If manga source is known update source TextView.
manga_genres.text = manga.genre if (source != null) {
manga_source.text = source.toString()
}
// Update status TextView. // Update genres TextView.
manga_status.setText(when (manga.status) { manga_genres.text = manga.genre
SManga.ONGOING -> R.string.ongoing
SManga.COMPLETED -> R.string.completed
SManga.LICENSED -> R.string.licensed
else -> R.string.unknown
})
// Update description TextView. // Update status TextView.
manga_summary.text = manga.description manga_status.setText(when (manga.status) {
SManga.ONGOING -> R.string.ongoing
SManga.COMPLETED -> R.string.completed
SManga.LICENSED -> R.string.licensed
else -> R.string.unknown
})
// Set the favorite drawable to the correct one. // Update description TextView.
setFavoriteDrawable(manga.favorite) manga_summary.text = manga.description
// Set cover if it wasn't already. // Set the favorite drawable to the correct one.
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { setFavoriteDrawable(manga.favorite)
GlideApp.with(context)
// Set cover if it wasn't already.
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(manga_cover)
if (backdrop != null) {
GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(manga_cover) .into(backdrop)
if (backdrop != null) {
GlideApp.with(context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(backdrop)
}
} }
} }
} }
@ -178,7 +174,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
* @param count number of chapters. * @param count number of chapters.
*/ */
fun setChapterCount(count: Float) { fun setChapterCount(count: Float) {
view?.manga_chapters?.text = DecimalFormat("#.#").format(count) manga_chapters?.text = DecimalFormat("#.#").format(count)
} }
/** /**
@ -243,10 +239,10 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
private fun setFavoriteDrawable(isFavorite: Boolean) { private fun setFavoriteDrawable(isFavorite: Boolean) {
// Set the Favorite drawable to the correct one. // Set the Favorite drawable to the correct one.
// Border drawable if false, filled drawable if true. // Border drawable if false, filled drawable if true.
view?.fab_favorite?.setImageResource(if (isFavorite) fab_favorite?.setImageResource(if (isFavorite)
R.drawable.ic_bookmark_white_24dp R.drawable.ic_bookmark_white_24dp
else else
R.drawable.ic_bookmark_border_white_24dp) R.drawable.ic_add_to_library_24dp)
} }
/** /**
@ -279,7 +275,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
* @param value whether it should be refreshing or not. * @param value whether it should be refreshing or not.
*/ */
private fun setRefreshing(value: Boolean) { private fun setRefreshing(value: Boolean) {
view?.swipe_refresh?.isRefreshing = value swipe_refresh?.isRefreshing = value
} }
/** /**
@ -305,6 +301,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
.showDialog(router) .showDialog(router)
} }
} }
activity?.toast(activity?.getString(R.string.manga_added_library))
}else{
activity?.toast(activity?.getString(R.string.manga_removed_library))
} }
} }

View File

@ -115,14 +115,14 @@ class MangaInfoPresenter(
* Returns true if the manga has any downloads. * Returns true if the manga has any downloads.
*/ */
fun hasDownloads(): Boolean { fun hasDownloads(): Boolean {
return downloadManager.findMangaDir(source, manga) != null return downloadManager.getDownloadCount(manga) > 0
} }
/** /**
* Deletes all the downloads for the manga. * Deletes all the downloads for the manga.
*/ */
fun deleteDownloads() { fun deleteDownloads() {
downloadManager.findMangaDir(source, manga)?.delete() downloadManager.deleteManga(manga, source)
} }
/** /**

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.manga.track package eu.kanade.tachiyomi.ui.manga.track
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -11,7 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.track_controller.view.* import kotlinx.android.synthetic.main.track_controller.*
class TrackController : NucleusController<TrackPresenter>(), class TrackController : NucleusController<TrackPresenter>(),
TrackAdapter.OnRowClickListener, TrackAdapter.OnRowClickListener,
@ -35,8 +34,8 @@ class TrackController : NucleusController<TrackPresenter>(),
return inflater.inflate(R.layout.track_controller, container, false) return inflater.inflate(R.layout.track_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
adapter = TrackAdapter(this) adapter = TrackAdapter(this)
with(view) { with(view) {
@ -48,14 +47,14 @@ class TrackController : NucleusController<TrackPresenter>(),
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null adapter = null
super.onDestroyView(view)
} }
fun onNextTrackings(trackings: List<TrackItem>) { fun onNextTrackings(trackings: List<TrackItem>) {
val atLeastOneLink = trackings.any { it.track != null } val atLeastOneLink = trackings.any { it.track != null }
adapter?.items = trackings adapter?.items = trackings
view?.swipe_refresh?.isEnabled = atLeastOneLink swipe_refresh?.isEnabled = atLeastOneLink
(parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
} }
@ -73,11 +72,11 @@ class TrackController : NucleusController<TrackPresenter>(),
} }
fun onRefreshDone() { fun onRefreshDone() {
view?.swipe_refresh?.isRefreshing = false swipe_refresh?.isRefreshing = false
} }
fun onRefreshError(error: Throwable) { fun onRefreshError(error: Throwable) {
view?.swipe_refresh?.isRefreshing = false swipe_refresh?.isRefreshing = false
activity?.toast(error.message) activity?.toast(error.message)
} }
@ -109,17 +108,17 @@ class TrackController : NucleusController<TrackPresenter>(),
override fun setStatus(item: TrackItem, selection: Int) { override fun setStatus(item: TrackItem, selection: Int) {
presenter.setStatus(item, selection) presenter.setStatus(item, selection)
view?.swipe_refresh?.isRefreshing = true swipe_refresh?.isRefreshing = true
} }
override fun setScore(item: TrackItem, score: Int) { override fun setScore(item: TrackItem, score: Int) {
presenter.setScore(item, score) presenter.setScore(item, score)
view?.swipe_refresh?.isRefreshing = true swipe_refresh?.isRefreshing = true
} }
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
presenter.setLastChapterRead(item, chaptersRead) presenter.setLastChapterRead(item, chaptersRead)
view?.swipe_refresh?.isRefreshing = true swipe_refresh?.isRefreshing = true
} }
private companion object { private companion object {

View File

@ -1,29 +1,29 @@
package eu.kanade.tachiyomi.ui.manga.track package eu.kanade.tachiyomi.ui.manga.track
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.track_item.view.* import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import kotlinx.android.synthetic.main.track_item.*
class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(view) { class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
init { init {
val listener = adapter.rowClickListener val listener = adapter.rowClickListener
view.title_container.setOnClickListener { listener.onTitleClick(adapterPosition) } title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
view.status_container.setOnClickListener { listener.onStatusClick(adapterPosition) } status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
view.chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) } chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
view.score_container.setOnClickListener { listener.onScoreClick(adapterPosition) } score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun bind(item: TrackItem) = with(itemView) { fun bind(item: TrackItem) {
val track = item.track val track = item.track
track_logo.setImageResource(item.service.getLogo()) track_logo.setImageResource(item.service.getLogo())
logo.setBackgroundColor(item.service.getLogoColor()) logo.setBackgroundColor(item.service.getLogoColor())
if (track != null) { if (track != null) {
track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary) track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary)
track_title.setAllCaps(false) track_title.setAllCaps(false)
track_title.text = track.title track_title.text = track.title
track_chapters.text = "${track.last_chapter_read}/" + track_chapters.text = "${track.last_chapter_read}/" +
@ -31,7 +31,7 @@ class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(v
track_status.text = item.service.getStatus(track.status) track_status.text = item.service.getStatus(track.status)
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
} else { } else {
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button) track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button)
track_title.setText(R.string.action_edit) track_title.setText(R.string.action_edit)
track_chapters.text = "" track_chapters.text = ""
track_score.text = "" track_score.text = ""

View File

@ -79,7 +79,7 @@ class ChapterLoader(
private fun retrievePageList(chapter: ReaderChapter) = Observable.just(chapter) private fun retrievePageList(chapter: ReaderChapter) = Observable.just(chapter)
.flatMap { .flatMap {
// Check if the chapter is downloaded. // Check if the chapter is downloaded.
chapter.isDownloaded = downloadManager.findChapterDir(source, manga, chapter) != null chapter.isDownloaded = downloadManager.isChapterDownloaded(chapter, manga, true)
if (chapter.isDownloaded) { if (chapter.isDownloaded) {
// Fetch the page list from disk. // Fetch the page list from disk.

View File

@ -64,7 +64,7 @@ class ReaderCustomFilterDialog : DialogFragment() {
* @param savedState The last saved instance state of the Fragment. * @param savedState The last saved instance state of the Fragment.
*/ */
override fun onCreateDialog(savedState: Bundle?): Dialog { override fun onCreateDialog(savedState: Bundle?): Dialog {
val dialog = MaterialDialog.Builder(activity) val dialog = MaterialDialog.Builder(activity!!)
.customView(R.layout.reader_custom_filter_dialog, false) .customView(R.layout.reader_custom_filter_dialog, false)
.positiveText(android.R.string.ok) .positiveText(android.R.string.ok)
.build() .build()

View File

@ -411,7 +411,7 @@ class ReaderPresenter(
fun deleteChapter(chapter: ReaderChapter, manga: Manga) { fun deleteChapter(chapter: ReaderChapter, manga: Manga) {
chapter.isDownloaded = false chapter.isDownloaded = false
chapter.pages?.forEach { it.status == Page.QUEUE } chapter.pages?.forEach { it.status == Page.QUEUE }
downloadManager.deleteChapter(source, manga, chapter) downloadManager.deleteChapter(chapter, manga, source)
} }
/** /**

View File

@ -24,7 +24,7 @@ class ReaderSettingsDialog : DialogFragment() {
private lateinit var subscriptions: CompositeSubscription private lateinit var subscriptions: CompositeSubscription
override fun onCreateDialog(savedState: Bundle?): Dialog { override fun onCreateDialog(savedState: Bundle?): Dialog {
val dialog = MaterialDialog.Builder(activity) val dialog = MaterialDialog.Builder(activity!!)
.title(R.string.label_settings) .title(R.string.label_settings)
.customView(R.layout.reader_settings_dialog, true) .customView(R.layout.reader_settings_dialog, true)
.positiveText(android.R.string.ok) .positiveText(android.R.string.ok)
@ -40,8 +40,11 @@ class ReaderSettingsDialog : DialogFragment() {
viewer.onItemSelectedListener = IgnoreFirstSpinnerListener { position -> viewer.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
subscriptions += Observable.timer(250, MILLISECONDS, AndroidSchedulers.mainThread()) subscriptions += Observable.timer(250, MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe { .subscribe {
(activity as ReaderActivity).presenter.updateMangaViewer(position) val readerActivity = activity as? ReaderActivity
activity.recreate() if (readerActivity != null) {
readerActivity.presenter.updateMangaViewer(position)
readerActivity.recreate()
}
} }
} }
viewer.setSelection((activity as ReaderActivity).presenter.manga.viewer, false) viewer.setSelection((activity as ReaderActivity).presenter.manga.viewer, false)

View File

@ -100,12 +100,12 @@ abstract class PagerReader : BaseReader() {
/** /**
* Text color for black theme. * Text color for black theme.
*/ */
val whiteColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryDark) } val whiteColor by lazy { ContextCompat.getColor(context!!, R.color.textColorSecondaryDark) }
/** /**
* Text color for white theme. * Text color for white theme.
*/ */
val blackColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryLight) } val blackColor by lazy { ContextCompat.getColor(context!!, R.color.textColorSecondaryLight) }
/** /**
* Initializes the pager. * Initializes the pager.
@ -118,7 +118,7 @@ abstract class PagerReader : BaseReader() {
this.pager = pager.apply { this.pager = pager.apply {
setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
setOffscreenPageLimit(1) setOffscreenPageLimit(1)
setId(R.id.view_pager) setId(R.id.reader_pager)
setOnChapterBoundariesOutListener(object : OnChapterBoundariesOutListener { setOnChapterBoundariesOutListener(object : OnChapterBoundariesOutListener {
override fun onFirstPageOutEvent() { override fun onFirstPageOutEvent() {
readerActivity.requestPreviousChapter() readerActivity.requestPreviousChapter()

View File

@ -24,7 +24,7 @@ class HorizontalPager(context: Context) : ViewPager(context), Pager {
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
try { try {
if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_DOWN) { if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_DOWN) {
if (currentItem == 0 || currentItem == adapter.count - 1) { if (currentItem == 0 || currentItem == adapter!!.count - 1) {
startDragX = ev.x startDragX = ev.x
} }
} }
@ -50,7 +50,7 @@ class HorizontalPager(context: Context) : ViewPager(context), Pager {
startDragX = 0f startDragX = 0f
} }
} else if (currentItem == adapter.count - 1) { } else if (currentItem == adapter!!.count - 1) {
if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_UP) { if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_UP) {
val displacement = startDragX - ev.x val displacement = startDragX - ev.x

View File

@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader
class LeftToRightReader : PagerReader() { class LeftToRightReader : PagerReader() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
return HorizontalPager(activity).apply { initializePager(this) } return HorizontalPager(activity!!).apply { initializePager(this) }
} }
} }

View File

@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader
class RightToLeftReader : PagerReader() { class RightToLeftReader : PagerReader() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
return HorizontalPager(activity).apply { return HorizontalPager(activity!!).apply {
rotation = 180f rotation = 180f
initializePager(this) initializePager(this)
} }

View File

@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader
class VerticalReader : PagerReader() { class VerticalReader : PagerReader() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
return VerticalPager(activity).apply { initializePager(this) } return VerticalPager(activity!!).apply { initializePager(this) }
} }
} }

View File

@ -33,7 +33,6 @@ import android.support.annotation.DrawableRes;
import android.support.v4.os.ParcelableCompat; import android.support.v4.os.ParcelableCompat;
import android.support.v4.os.ParcelableCompatCreatorCallbacks; import android.support.v4.os.ParcelableCompatCreatorCallbacks;
import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.KeyEventCompat;
import android.support.v4.view.MotionEventCompat; import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.PagerAdapter; import android.support.v4.view.PagerAdapter;
import android.support.v4.view.VelocityTrackerCompat; import android.support.v4.view.VelocityTrackerCompat;
@ -2598,14 +2597,10 @@ public class VerticalViewPagerImpl extends ViewGroup {
handled = arrowScroll(FOCUS_RIGHT); handled = arrowScroll(FOCUS_RIGHT);
break; break;
case KeyEvent.KEYCODE_TAB: case KeyEvent.KEYCODE_TAB:
if (Build.VERSION.SDK_INT >= 11) { if (event.hasNoModifiers()) {
// The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD handled = arrowScroll(FOCUS_FORWARD);
// before Android 3.0. Ignore the tab key on those devices. } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
if (KeyEventCompat.hasNoModifiers(event)) { handled = arrowScroll(FOCUS_BACKWARD);
handled = arrowScroll(FOCUS_FORWARD);
} else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
handled = arrowScroll(FOCUS_BACKWARD);
}
} }
break; break;
default: default:

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.support.v7.widget.RecyclerView
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -11,10 +10,11 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import kotlinx.android.synthetic.main.reader_webtoon_item.view.* import kotlinx.android.synthetic.main.reader_webtoon_item.*
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit
* @constructor creates a new webtoon holder. * @constructor creates a new webtoon holder.
*/ */
class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) : class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) :
RecyclerView.ViewHolder(view) { BaseViewHolder(view) {
/** /**
* Page of a chapter. * Page of a chapter.
@ -54,7 +54,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
private var decodeErrorLayout: View? = null private var decodeErrorLayout: View? = null
init { init {
with(view.image_view) { with(image_view) {
setMaxTileSize(readerActivity.maxBitmapSize) setMaxTileSize(readerActivity.maxBitmapSize)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED) setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
@ -78,11 +78,11 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
}) })
} }
view.progress_container.layoutParams = FrameLayout.LayoutParams( progress_container.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, webtoonReader.screenHeight) MATCH_PARENT, webtoonReader.screenHeight)
view.setOnTouchListener(adapter.touchListener) view.setOnTouchListener(adapter.touchListener)
view.retry_button.setOnTouchListener { _, event -> retry_button.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) { if (event.action == MotionEvent.ACTION_UP) {
readerActivity.presenter.retryPage(page) readerActivity.presenter.retryPage(page)
} }
@ -111,9 +111,9 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
(view as ViewGroup).removeView(it) (view as ViewGroup).removeView(it)
decodeErrorLayout = null decodeErrorLayout = null
} }
view.image_view.recycle() image_view.recycle()
view.image_view.visibility = View.GONE image_view.visibility = View.GONE
view.progress_container.visibility = View.VISIBLE progress_container.visibility = View.VISIBLE
} }
/** /**
@ -150,7 +150,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
.onBackpressureLatest() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { progress -> .subscribe { progress ->
view.progress_text.text = if (progress > 0) { progress_text.text = if (progress > 0) {
view.context.getString(R.string.download_progress, progress) view.context.getString(R.string.download_progress, progress)
} else { } else {
view.context.getString(R.string.downloading) view.context.getString(R.string.downloading)
@ -279,14 +279,14 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
* Called when the image is decoded and going to be displayed. * Called when the image is decoded and going to be displayed.
*/ */
private fun onImageDecoded() { private fun onImageDecoded() {
view.progress_container.visibility = View.GONE progress_container.visibility = View.GONE
} }
/** /**
* Called when the image fails to decode. * Called when the image fails to decode.
*/ */
private fun onImageDecodeError() { private fun onImageDecodeError() {
view.progress_container.visibility = View.GONE progress_container.visibility = View.GONE
val page = page ?: return val page = page ?: return
if (decodeErrorLayout != null || !webtoonReader.isAdded) return if (decodeErrorLayout != null || !webtoonReader.isAdded) return

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