mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
634356e72f | |||
6d3cc16ab1 | |||
6027671c09 | |||
29d0cb4a15 | |||
fe7001975a | |||
ac88f1c146 | |||
b5b86218c5 | |||
bdcc6e52e6 | |||
0eae817aa6 | |||
8994b42760 | |||
6a63ce992a | |||
9ae6285eef | |||
8f9737f567 | |||
f287d313c3 | |||
e745836404 | |||
08baf798aa | |||
8bcb14c65d | |||
d94dc68830 | |||
297fed6aef | |||
d690d6e0e3 | |||
9ba8d88b07 | |||
34a40b0131 | |||
182bf5f2bd | |||
04638535d8 | |||
d87c8428fe | |||
166fb9a8e4 | |||
28a21d0b8f | |||
d1d1d60c30 | |||
80fd49d60b | |||
34eb1331a3 | |||
bff329a329 | |||
604929d002 |
BIN
.github/readme-images/app-icon.png
vendored
Normal file
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
BIN
.github/readme-images/screens.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1022 KiB |
@ -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
|
||||||
|
@ -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
|
||||||
|
39
README.md
39
README.md
@ -1,25 +1,38 @@
|
|||||||
| Build | Download | F-Droid | Contribute | Contact |
|
| Build | Download | F-Droid | Contribute | Contact |
|
||||||
|-------|----------|---------|------------|---------|
|
|-------|----------|---------|------------|---------|
|
||||||
| [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [](https://github.com/inorichi/tachiyomi/wiki/Translation) | [](https://discord.gg/WrBkRk4) |
|
| [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [](https://github.com/inorichi/tachiyomi/wiki/Translation) | [](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.
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
|
# 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.
|

|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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("ёнкома")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>()
|
||||||
|
@ -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>()
|
||||||
|
@ -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) {
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
@ -1,3 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.catalogue
|
|
||||||
|
|
||||||
class NoResultsException : Exception()
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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)
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
@ -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
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||||
|
|
||||||
|
class NoResultsException : Exception()
|
@ -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
|
@ -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
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 {
|
||||||
|
@ -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 = ""
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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
Reference in New Issue
Block a user