mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-28 12:07:52 +02:00
Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
9bcde69ee0 | |||
beca2b429c | |||
3a1699f0b3 | |||
a7192e866f | |||
dc882b4dce | |||
77b4de3941 | |||
006d17aac7 | |||
1a3a1db4ff | |||
97fa659283 | |||
f1d84ccb49 | |||
1f14240251 | |||
ea6fed6ecf | |||
d09eca7833 | |||
2c6f64c5ae | |||
ec87e4359b | |||
2b63bae989 | |||
82f4e3157a | |||
725ceab00b | |||
ba428c401d | |||
510669ee2c | |||
8d749df290 | |||
091c0c0c71 | |||
7fdd2cacd7 | |||
2241a0b2de | |||
d21a93123b | |||
e542a8d8e2 | |||
94ee4e7fb5 | |||
e3d430eb5e | |||
fd76255cf6 | |||
d180631877 | |||
1977e21363 | |||
e1a3ee1b81 | |||
cc43d9daed | |||
79705df499 | |||
36bbb906c1 | |||
816cc17ed3 | |||
97e3b5d2ab | |||
79ab9d80f2 | |||
32511149d1 | |||
cc9fd53abb | |||
4061c7450b | |||
9ad535bde6 | |||
b067096fc7 | |||
2dd58e5f7d | |||
7c42ab885b | |||
26b283d44d | |||
8c1b07c4ba | |||
f98e0858a7 | |||
8b60d5bfcb | |||
30b4c6e755 | |||
3d2a98451b | |||
aba528b227 |
12
.travis.yml
12
.travis.yml
@ -12,11 +12,21 @@ android:
|
|||||||
- extra-android-support
|
- extra-android-support
|
||||||
- extra-google-google_play_services
|
- extra-google-google_play_services
|
||||||
|
|
||||||
|
licenses:
|
||||||
|
- android-sdk-license-.+
|
||||||
|
- '.+'
|
||||||
|
|
||||||
jdk:
|
jdk:
|
||||||
- oraclejdk8
|
- oraclejdk8
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- chmod +x gradlew
|
- chmod +x gradlew
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- mkdir "$ANDROID_HOME/licenses" || true
|
||||||
|
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||||
|
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||||
|
|
||||||
#Build, and run tests
|
#Build, and run tests
|
||||||
script: "./gradlew clean buildStandardDebug"
|
script: "./gradlew clean buildStandardDebug"
|
||||||
sudo: false
|
sudo: false
|
||||||
|
@ -38,8 +38,8 @@ android {
|
|||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 25
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 16
|
versionCode 18
|
||||||
versionName "0.4.0"
|
versionName "0.4.2"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@ -98,7 +98,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified dependencies
|
// Modified dependencies
|
||||||
compile 'com.github.inorichi:subsampling-scale-image-view:f687b74'
|
compile 'com.github.inorichi:subsampling-scale-image-view:c4db85c'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
final support_library_version = '25.0.1'
|
final support_library_version = '25.0.1'
|
||||||
@ -110,14 +110,16 @@ dependencies {
|
|||||||
compile "com.android.support:support-annotations:$support_library_version"
|
compile "com.android.support:support-annotations:$support_library_version"
|
||||||
compile "com.android.support:customtabs:$support_library_version"
|
compile "com.android.support:customtabs:$support_library_version"
|
||||||
|
|
||||||
|
compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'
|
||||||
|
|
||||||
compile 'com.android.support:multidex:1.0.1'
|
compile 'com.android.support:multidex:1.0.1'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.1'
|
compile 'io.reactivex:rxandroid:1.2.1'
|
||||||
compile 'io.reactivex:rxjava:1.2.3'
|
compile 'io.reactivex:rxjava:1.2.4'
|
||||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
compile 'com.github.pwittchen:reactivenetwork:0.6.0'
|
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:3.5.0"
|
compile "com.squareup.okhttp3:okhttp:3.5.0"
|
||||||
@ -172,7 +174,7 @@ dependencies {
|
|||||||
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.3.1'
|
compile 'com.jakewharton.timber:timber:4.4.0'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
compile 'ch.acra:acra:4.9.1'
|
compile 'ch.acra:acra:4.9.1'
|
||||||
@ -202,7 +204,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.0.5-2'
|
ext.kotlin_version = '1.0.6'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,18 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerTheme">
|
android:theme="@style/FilePickerTheme">
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.AnilistLoginActivity"
|
||||||
|
android:label="Anilist">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data
|
||||||
|
android:host="anilist-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
@ -70,7 +82,7 @@
|
|||||||
<service android:name=".data.download.DownloadService"
|
<service android:name=".data.download.DownloadService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
<service android:name=".data.track.TrackUpdateService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<service android:name=".data.updater.UpdateDownloaderService"
|
<service android:name=".data.updater.UpdateDownloaderService"
|
||||||
|
@ -2,10 +2,12 @@ package eu.kanade.tachiyomi
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.support.multidex.MultiDex
|
import android.support.multidex.MultiDex
|
||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||||
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.annotation.ReportsCrashes
|
import org.acra.annotation.ReportsCrashes
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -18,7 +20,7 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
|||||||
reportType = org.acra.sender.HttpSender.Type.JSON,
|
reportType = org.acra.sender.HttpSender.Type.JSON,
|
||||||
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
||||||
buildConfigClass = BuildConfig::class,
|
buildConfigClass = BuildConfig::class,
|
||||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*")
|
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
||||||
)
|
)
|
||||||
open class App : Application() {
|
open class App : Application() {
|
||||||
|
|
||||||
@ -31,6 +33,8 @@ open class App : Application() {
|
|||||||
|
|
||||||
setupAcra()
|
setupAcra()
|
||||||
setupJobManager()
|
setupJobManager()
|
||||||
|
|
||||||
|
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
@ -40,6 +44,11 @@ open class App : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
protected open fun setupAcra() {
|
||||||
ACRA.init(this)
|
ACRA.init(this)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
|
|||||||
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
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
@ -32,7 +32,7 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { DownloadManager(app) }
|
addSingletonFactory { DownloadManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { MangaSyncManager(app) }
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { Gson() }
|
addSingletonFactory { Gson() }
|
||||||
|
|
||||||
|
@ -6,4 +6,5 @@ object Constants {
|
|||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
|
||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
|
||||||
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5
|
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
private val MANGA = "manga"
|
private val MANGA = "manga"
|
||||||
private val MANGAS = "mangas"
|
private val MANGAS = "mangas"
|
||||||
private val CHAPTERS = "chapters"
|
private val CHAPTERS = "chapters"
|
||||||
private val MANGA_SYNC = "sync"
|
private val TRACK = "sync"
|
||||||
private val CATEGORIES = "categories"
|
private val CATEGORIES = "categories"
|
||||||
|
|
||||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||||
@ -109,10 +109,10 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup manga sync
|
// Backup tracks
|
||||||
val mangaSync = db.getMangasSync(manga).executeAsBlocking()
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
if (!mangaSync.isEmpty()) {
|
if (!tracks.isEmpty()) {
|
||||||
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync))
|
entry.add(TRACK, gson.toJsonTree(tracks))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup categories for this manga
|
// Backup categories for this manga
|
||||||
@ -231,13 +231,13 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
val element = backupManga.asJsonObject
|
val element = backupManga.asJsonObject
|
||||||
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
|
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
|
||||||
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
|
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
|
||||||
val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray())
|
val tracks = gson.fromJson<List<TrackImpl>>(element.get(TRACK) ?: JsonArray())
|
||||||
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
|
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
|
||||||
|
|
||||||
// Restore everything related to this manga
|
// Restore everything related to this manga
|
||||||
restoreManga(manga)
|
restoreManga(manga)
|
||||||
restoreChaptersForManga(manga, chapters)
|
restoreChaptersForManga(manga, chapters)
|
||||||
restoreSyncForManga(manga, sync)
|
restoreSyncForManga(manga, tracks)
|
||||||
restoreCategoriesForManga(manga, categories)
|
restoreCategoriesForManga(manga, categories)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,35 +333,35 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
* Restores the sync of a manga.
|
* Restores the sync of a manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga whose sync have to be restored.
|
* @param manga the manga whose sync have to be restored.
|
||||||
* @param sync the sync to restore.
|
* @param tracks the track list to restore.
|
||||||
*/
|
*/
|
||||||
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
private fun restoreSyncForManga(manga: Manga, tracks: List<Track>) {
|
||||||
// Fix foreign keys with the current manga id
|
// Fix foreign keys with the current manga id
|
||||||
for (mangaSync in sync) {
|
for (track in tracks) {
|
||||||
mangaSync.manga_id = manga.id!!
|
track.manga_id = manga.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
val dbTracks = db.getTracks(manga).executeAsBlocking()
|
||||||
val syncToUpdate = ArrayList<MangaSync>()
|
val trackToUpdate = ArrayList<Track>()
|
||||||
for (backupSync in sync) {
|
for (backupTrack in tracks) {
|
||||||
// Try to find existing chapter in db
|
// Try to find existing chapter in db
|
||||||
val pos = dbSyncs.indexOf(backupSync)
|
val pos = dbTracks.indexOf(backupTrack)
|
||||||
if (pos != -1) {
|
if (pos != -1) {
|
||||||
// The sync is already in the db, only update its fields
|
// The sync is already in the db, only update its fields
|
||||||
val dbSync = dbSyncs[pos]
|
val dbSync = dbTracks[pos]
|
||||||
// Mark the max chapter as read and nothing else
|
// Mark the max chapter as read and nothing else
|
||||||
dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read)
|
dbSync.last_chapter_read = Math.max(backupTrack.last_chapter_read, dbSync.last_chapter_read)
|
||||||
syncToUpdate.add(dbSync)
|
trackToUpdate.add(dbSync)
|
||||||
} else {
|
} else {
|
||||||
// Insert new sync. Let the db assign the id
|
// Insert new sync. Let the db assign the id
|
||||||
backupSync.id = null
|
backupTrack.id = null
|
||||||
syncToUpdate.add(backupSync)
|
trackToUpdate.add(backupTrack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
if (!syncToUpdate.isEmpty()) {
|
if (!trackToUpdate.isEmpty()) {
|
||||||
db.insertMangasSync(syncToUpdate).executeAsBlocking()
|
db.insertTracks(trackToUpdate).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import com.google.gson.FieldAttributes
|
|||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
|
||||||
class IdExclusion : ExclusionStrategy {
|
class IdExclusion : ExclusionStrategy {
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ class IdExclusion : ExclusionStrategy {
|
|||||||
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
||||||
MangaImpl::class.java -> mangaExclusions.contains(f.name)
|
MangaImpl::class.java -> mangaExclusions.contains(f.name)
|
||||||
ChapterImpl::class.java -> chapterExclusions.contains(f.name)
|
ChapterImpl::class.java -> chapterExclusions.contains(f.name)
|
||||||
MangaSyncImpl::class.java -> syncExclusions.contains(f.name)
|
TrackImpl::class.java -> syncExclusions.contains(f.name)
|
||||||
CategoryImpl::class.java -> categoryExclusions.contains(f.name)
|
CategoryImpl::class.java -> categoryExclusions.contains(f.name)
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,13 @@ import eu.kanade.tachiyomi.data.database.queries.*
|
|||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context)
|
open class DatabaseHelper(context: Context)
|
||||||
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||||
|
|
||||||
override val db = DefaultStorIOSQLite.builder()
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
.sqliteOpenHelper(DbOpenHelper(context))
|
.sqliteOpenHelper(DbOpenHelper(context))
|
||||||
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||||
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||||
.addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping())
|
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||||
|
@ -23,7 +23,7 @@ class DbOpenHelper(context: Context)
|
|||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||||
execSQL(MangaTable.createTableQuery)
|
execSQL(MangaTable.createTableQuery)
|
||||||
execSQL(ChapterTable.createTableQuery)
|
execSQL(ChapterTable.createTableQuery)
|
||||||
execSQL(MangaSyncTable.createTableQuery)
|
execSQL(TrackTable.createTableQuery)
|
||||||
execSQL(CategoryTable.createTableQuery)
|
execSQL(CategoryTable.createTableQuery)
|
||||||
execSQL(MangaCategoryTable.createTableQuery)
|
execSQL(MangaCategoryTable.createTableQuery)
|
||||||
execSQL(HistoryTable.createTableQuery)
|
execSQL(HistoryTable.createTableQuery)
|
||||||
|
@ -9,38 +9,38 @@ import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_REMOTE_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SCORE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SYNC_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TITLE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TOTAL_CHAPTERS
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
||||||
|
|
||||||
class MangaSyncTypeMapping : SQLiteTypeMapping<MangaSync>(
|
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||||
MangaSyncPutResolver(),
|
TrackPutResolver(),
|
||||||
MangaSyncGetResolver(),
|
TrackGetResolver(),
|
||||||
MangaSyncDeleteResolver()
|
TrackDeleteResolver()
|
||||||
)
|
)
|
||||||
|
|
||||||
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
|
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder()
|
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: MangaSync) = ContentValues(9).apply {
|
override fun mapToContentValues(obj: Track) = ContentValues(9).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
put(COL_SYNC_ID, obj.sync_id)
|
put(COL_SYNC_ID, obj.sync_id)
|
||||||
@ -53,9 +53,9 @@ class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
|
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
||||||
@ -68,9 +68,9 @@ class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() {
|
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder()
|
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
|
|||||||
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface MangaSync : Serializable {
|
interface Track : Serializable {
|
||||||
|
|
||||||
var id: Long?
|
var id: Long?
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ interface MangaSync : Serializable {
|
|||||||
|
|
||||||
var update: Boolean
|
var update: Boolean
|
||||||
|
|
||||||
fun copyPersonalFrom(other: MangaSync) {
|
fun copyPersonalFrom(other: Track) {
|
||||||
last_chapter_read = other.last_chapter_read
|
last_chapter_read = other.last_chapter_read
|
||||||
score = other.score
|
score = other.score
|
||||||
status = other.status
|
status = other.status
|
||||||
@ -32,7 +32,7 @@ interface MangaSync : Serializable {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply {
|
fun create(serviceId: Int): Track = TrackImpl().apply {
|
||||||
sync_id = serviceId
|
sync_id = serviceId
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
class MangaSyncImpl : MangaSync {
|
class TrackImpl : Track {
|
||||||
|
|
||||||
override var id: Long? = null
|
override var id: Long? = null
|
||||||
|
|
||||||
@ -26,11 +26,11 @@ class MangaSyncImpl : MangaSync {
|
|||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val mangaSync = other as MangaSync
|
other as Track
|
||||||
|
|
||||||
if (manga_id != mangaSync.manga_id) return false
|
if (manga_id != other.manga_id) return false
|
||||||
if (sync_id != mangaSync.sync_id) return false
|
if (sync_id != other.sync_id) return false
|
||||||
return remote_id == mangaSync.remote_id
|
return remote_id == other.remote_id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
@ -40,7 +40,6 @@ interface HistoryQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the history last read.
|
* Updates the history last read.
|
||||||
* Inserts history object if not yet in database
|
* Inserts history object if not yet in database
|
||||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
@ -29,7 +30,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
open fun getFavoriteMangas() = db.get()
|
fun getFavoriteMangas() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(Query.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
@ -66,6 +67,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFlagsPutResolver())
|
.withPutResolver(MangaFlagsPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
@ -78,4 +84,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getLastReadManga() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getLastReadMangaQuery())
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
}
|
}
|
@ -1,46 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
|
|
||||||
interface MangaSyncQueries : DbProvider {
|
|
||||||
|
|
||||||
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
|
|
||||||
.`object`(MangaSync::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
|
|
||||||
"${MangaSyncTable.COL_SYNC_ID} = ?")
|
|
||||||
.whereArgs(manga.id, sync.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getMangasSync(manga: Manga) = db.get()
|
|
||||||
.listOfObjects(MangaSync::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
|
||||||
|
|
||||||
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
|
||||||
|
|
||||||
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
|
||||||
|
|
||||||
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
}
|
|
@ -73,6 +73,18 @@ fun getHistoryByMangaId() = """
|
|||||||
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fun getLastReadMangaQuery() = """
|
||||||
|
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
JOIN ${History.TABLE}
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
|
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
|
||||||
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
|
ORDER BY max DESC
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the categories for a manga.
|
* Query to get the categories for a manga.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
|
||||||
|
interface TrackQueries : DbProvider {
|
||||||
|
|
||||||
|
fun getTracks(manga: Manga) = db.get()
|
||||||
|
.listOfObjects(Track::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(TrackTable.TABLE)
|
||||||
|
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
||||||
|
|
||||||
|
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
||||||
|
|
||||||
|
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(TrackTable.TABLE)
|
||||||
|
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||||
|
.whereArgs(manga.id, sync.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
package eu.kanade.tachiyomi.data.database.tables
|
||||||
|
|
||||||
object MangaSyncTable {
|
object TrackTable {
|
||||||
|
|
||||||
const val TABLE = "manga_sync"
|
const val TABLE = "manga_sync"
|
||||||
|
|
@ -110,6 +110,15 @@ class DownloadManager(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the directory name for a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga to query.
|
||||||
|
*/
|
||||||
|
fun getMangaDirName(manga: Manga): String {
|
||||||
|
return provider.getMangaDirName(manga)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the directory name for the given chapter.
|
* Returns the directory name for the given chapter.
|
||||||
*
|
*
|
||||||
@ -119,6 +128,15 @@ class DownloadManager(context: Context) {
|
|||||||
return provider.getChapterDirName(chapter)
|
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.
|
* Returns the directory for the given manga, if it exists.
|
||||||
*
|
*
|
||||||
|
@ -6,6 +6,7 @@ import com.hippo.unifile.UniFile
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
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.source.Source
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -26,10 +27,13 @@ class DownloadProvider(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* The root directory for downloads.
|
* The root directory for downloads.
|
||||||
*/
|
*/
|
||||||
private lateinit var downloadsDir: UniFile
|
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
|
||||||
|
UniFile.fromUri(context, Uri.parse(it))
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.downloadsDirectory().asObservable()
|
preferences.downloadsDirectory().asObservable()
|
||||||
|
.skip(1)
|
||||||
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +49,15 @@ class DownloadProvider(private val context: Context) {
|
|||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getMangaDirName(manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the download directory for a source if it exists.
|
||||||
|
*
|
||||||
|
* @param source the source to query.
|
||||||
|
*/
|
||||||
|
fun findSourceDir(source: Source): UniFile? {
|
||||||
|
return downloadsDir.findFile(getSourceDirName(source))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the download directory for a manga if it exists.
|
* Returns the download directory for a manga if it exists.
|
||||||
*
|
*
|
||||||
@ -52,7 +65,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param manga the manga to query.
|
* @param manga the manga to query.
|
||||||
*/
|
*/
|
||||||
fun findMangaDir(source: Source, manga: Manga): UniFile? {
|
fun findMangaDir(source: Source, manga: Manga): UniFile? {
|
||||||
val sourceDir = downloadsDir.findFile(getSourceDirName(source))
|
val sourceDir = findSourceDir(source)
|
||||||
return sourceDir?.findFile(getMangaDirName(manga))
|
return sourceDir?.findFile(getMangaDirName(manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,10 @@ import eu.kanade.tachiyomi.Constants
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
@ -53,6 +56,8 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wake lock that will be held until the service is destroyed.
|
* Wake lock that will be held until the service is destroyed.
|
||||||
*/
|
*/
|
||||||
@ -243,32 +248,55 @@ class LibraryUpdateService : Service() {
|
|||||||
// If there's any error, return empty update and continue.
|
// If there's any error, return empty update and continue.
|
||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
failedUpdates.add(manga)
|
failedUpdates.add(manga)
|
||||||
Pair(0, 0)
|
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
||||||
}
|
}
|
||||||
// Filter out mangas without new chapters (or failed).
|
// Filter out mangas without new chapters (or failed).
|
||||||
.filter { pair -> pair.first > 0 }
|
.filter { pair -> pair.first.size > 0 }
|
||||||
|
.doOnNext {
|
||||||
|
if (preferences.downloadNew()) {
|
||||||
|
downloadChapters(manga, it.first)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Convert to the manga that contains new chapters.
|
// Convert to the manga that contains new chapters.
|
||||||
.map { manga }
|
.map { manga }
|
||||||
}
|
}
|
||||||
// Add manga with new chapters to the list.
|
// Add manga with new chapters to the list.
|
||||||
.doOnNext { newUpdates.add(it) }
|
.doOnNext { manga ->
|
||||||
|
// Set last updated time
|
||||||
|
manga.last_update = Date().time
|
||||||
|
db.updateLastUpdated(manga).executeAsBlocking()
|
||||||
|
// Add to the list
|
||||||
|
newUpdates.add(manga)
|
||||||
|
}
|
||||||
// Notify result of the overall update.
|
// Notify result of the overall update.
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
if (newUpdates.isEmpty()) {
|
if (newUpdates.isEmpty()) {
|
||||||
cancelNotification()
|
cancelNotification()
|
||||||
} else {
|
} else {
|
||||||
|
if (preferences.downloadNew()) {
|
||||||
|
DownloadService.start(this)
|
||||||
|
}
|
||||||
showResultNotification(newUpdates, failedUpdates)
|
showResultNotification(newUpdates, failedUpdates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
|
// we need to get the chapters from the db so we have chapter ids
|
||||||
|
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
|
val dbChapters = chapters.map {
|
||||||
|
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
|
||||||
|
}
|
||||||
|
downloadManager.downloadChapters(manga, dbChapters)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the chapters for the given manga and adds them to the database.
|
* Updates the chapters for the given manga and adds them to the database.
|
||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
|
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: return Observable.empty()
|
val source = sourceManager.get(manga.source) as? OnlineSource ?: return Observable.empty()
|
||||||
return source.fetchChapterList(manga)
|
return source.fetchChapterList(manga)
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
|
|
||||||
|
|
||||||
class MangaSyncManager(private val context: Context) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val MYANIMELIST = 1
|
|
||||||
const val ANILIST = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
|
||||||
|
|
||||||
val aniList = Anilist(context, ANILIST)
|
|
||||||
|
|
||||||
// TODO enable anilist
|
|
||||||
val services = listOf(myAnimeList)
|
|
||||||
|
|
||||||
fun getService(id: Int) = services.find { it.id == id }
|
|
||||||
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.support.annotation.CallSuper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
|
||||||
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
|
||||||
val networkService: NetworkHelper by injectLazy()
|
|
||||||
|
|
||||||
open val client: OkHttpClient
|
|
||||||
get() = networkService.client
|
|
||||||
|
|
||||||
// Name of the manga sync service to display
|
|
||||||
abstract val name: String
|
|
||||||
|
|
||||||
abstract fun login(username: String, password: String): Completable
|
|
||||||
|
|
||||||
open val isLogged: Boolean
|
|
||||||
get() = !getUsername().isEmpty() &&
|
|
||||||
!getPassword().isEmpty()
|
|
||||||
|
|
||||||
abstract fun add(manga: MangaSync): Observable<MangaSync>
|
|
||||||
|
|
||||||
abstract fun update(manga: MangaSync): Observable<MangaSync>
|
|
||||||
|
|
||||||
abstract fun bind(manga: MangaSync): Observable<MangaSync>
|
|
||||||
|
|
||||||
abstract fun getStatus(status: Int): String
|
|
||||||
|
|
||||||
fun saveCredentials(username: String, password: String) {
|
|
||||||
preferences.setMangaSyncCredentials(this, username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
open fun logout() {
|
|
||||||
preferences.setMangaSyncCredentials(this, "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUsername() = preferences.mangaSyncUsername(this)
|
|
||||||
|
|
||||||
fun getPassword() = preferences.mangaSyncPassword(this)
|
|
||||||
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class Anilist(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val READING = 1
|
|
||||||
const val COMPLETED = 2
|
|
||||||
const val ON_HOLD = 3
|
|
||||||
const val DROPPED = 4
|
|
||||||
const val PLAN_TO_READ = 5
|
|
||||||
|
|
||||||
const val DEFAULT_STATUS = READING
|
|
||||||
const val DEFAULT_SCORE = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name = "AniList"
|
|
||||||
|
|
||||||
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
|
||||||
|
|
||||||
private val api by lazy {
|
|
||||||
AnilistApi.createService(networkService.client.newBuilder()
|
|
||||||
.addInterceptor(interceptor)
|
|
||||||
.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun login(username: String, password: String) = login(password)
|
|
||||||
|
|
||||||
fun login(authCode: String): Completable {
|
|
||||||
// Create a new api with the default client to avoid request interceptions.
|
|
||||||
return AnilistApi.createService(client)
|
|
||||||
// Request the access token from the API with the authorization code.
|
|
||||||
.requestAccessToken(authCode)
|
|
||||||
// Save the token in the interceptor.
|
|
||||||
.doOnNext { interceptor.setAuth(it) }
|
|
||||||
// Obtain the authenticated user from the API.
|
|
||||||
.zipWith(api.getCurrentUser().map { it["id"].toString() })
|
|
||||||
{ oauth, user -> Pair(user, oauth.refresh_token!!) }
|
|
||||||
// Save service credentials (username and refresh token).
|
|
||||||
.doOnNext { saveCredentials(it.first, it.second) }
|
|
||||||
// Logout on any error.
|
|
||||||
.doOnError { logout() }
|
|
||||||
.toCompletable()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun logout() {
|
|
||||||
super.logout()
|
|
||||||
interceptor.setAuth(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(query: String): Observable<List<MangaSync>> {
|
|
||||||
return api.search(query, 1)
|
|
||||||
.flatMap { Observable.from(it) }
|
|
||||||
.filter { it.type != "Novel" }
|
|
||||||
.map { it.toMangaSync() }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getList(): Observable<List<MangaSync>> {
|
|
||||||
return api.getList(getUsername())
|
|
||||||
.flatMap { Observable.from(it.flatten()) }
|
|
||||||
.map { it.toMangaSync() }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
return api.addManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
|
||||||
manga.score.toInt())
|
|
||||||
.doOnNext { it.body().close() }
|
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
|
||||||
.doOnError { Timber.e(it, it.message) }
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
|
||||||
manga.status = COMPLETED
|
|
||||||
}
|
|
||||||
return api.updateManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
|
||||||
manga.score.toInt())
|
|
||||||
.doOnNext { it.body().close() }
|
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
|
||||||
.doOnError { Timber.e(it, it.message) }
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
return getList()
|
|
||||||
.flatMap { userlist ->
|
|
||||||
manga.sync_id = id
|
|
||||||
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
|
||||||
if (mangaFromList != null) {
|
|
||||||
manga.copyPersonalFrom(mangaFromList)
|
|
||||||
update(manga)
|
|
||||||
} else {
|
|
||||||
// Set default fields if it's not found in the list
|
|
||||||
manga.score = DEFAULT_SCORE.toFloat()
|
|
||||||
manga.status = DEFAULT_STATUS
|
|
||||||
add(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
|
||||||
when (status) {
|
|
||||||
READING -> getString(R.string.reading)
|
|
||||||
COMPLETED -> getString(R.string.completed)
|
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
|
||||||
DROPPED -> getString(R.string.dropped)
|
|
||||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MangaSync.getAnilistStatus() = when (status) {
|
|
||||||
READING -> "reading"
|
|
||||||
COMPLETED -> "completed"
|
|
||||||
ON_HOLD -> "on-hold"
|
|
||||||
DROPPED -> "dropped"
|
|
||||||
PLAN_TO_READ -> "plan to read"
|
|
||||||
else -> throw NotImplementedError("Unknown status")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALManga
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALUserLists
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import retrofit2.Response
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import retrofit2.http.*
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
interface AnilistApi {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val clientId = "tachiyomi-hrtje"
|
|
||||||
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
|
||||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
|
||||||
private const val baseUrl = "https://anilist.co/api/"
|
|
||||||
|
|
||||||
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
|
||||||
.appendQueryParameter("grant_type", "authorization_code")
|
|
||||||
.appendQueryParameter("client_id", clientId)
|
|
||||||
.appendQueryParameter("redirect_uri", clientUrl)
|
|
||||||
.appendQueryParameter("response_type", "code")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
|
||||||
body = FormBody.Builder()
|
|
||||||
.add("grant_type", "refresh_token")
|
|
||||||
.add("client_id", clientId)
|
|
||||||
.add("client_secret", clientSecret)
|
|
||||||
.add("refresh_token", token)
|
|
||||||
.build())
|
|
||||||
|
|
||||||
fun createService(client: OkHttpClient) = Retrofit.Builder()
|
|
||||||
.baseUrl(baseUrl)
|
|
||||||
.client(client)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
|
||||||
.build()
|
|
||||||
.create(AnilistApi::class.java)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@POST("auth/access_token")
|
|
||||||
fun requestAccessToken(
|
|
||||||
@Field("code") code: String,
|
|
||||||
@Field("grant_type") grant_type: String = "authorization_code",
|
|
||||||
@Field("client_id") client_id: String = clientId,
|
|
||||||
@Field("client_secret") client_secret: String = clientSecret,
|
|
||||||
@Field("redirect_uri") redirect_uri: String = clientUrl)
|
|
||||||
: Observable<OAuth>
|
|
||||||
|
|
||||||
@GET("user")
|
|
||||||
fun getCurrentUser(): Observable<JsonObject>
|
|
||||||
|
|
||||||
@GET("manga/search/{query}")
|
|
||||||
fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>>
|
|
||||||
|
|
||||||
@GET("user/{username}/mangalist")
|
|
||||||
fun getList(@Path("username") username: String): Observable<ALUserLists>
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@PUT("mangalist")
|
|
||||||
fun addManga(
|
|
||||||
@Field("id") id: Int,
|
|
||||||
@Field("chapters_read") chapters_read: Int,
|
|
||||||
@Field("list_status") list_status: String,
|
|
||||||
@Field("score_raw") score_raw: Int)
|
|
||||||
: Observable<Response<ResponseBody>>
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@PUT("mangalist")
|
|
||||||
fun updateManga(
|
|
||||||
@Field("id") id: Int,
|
|
||||||
@Field("chapters_read") chapters_read: Int,
|
|
||||||
@Field("list_status") list_status: String,
|
|
||||||
@Field("score_raw") score_raw: Int)
|
|
||||||
: Observable<Response<ResponseBody>>
|
|
||||||
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
|
||||||
|
|
||||||
data class ALManga(
|
|
||||||
val id: Int,
|
|
||||||
val title_romaji: String,
|
|
||||||
val type: String,
|
|
||||||
val total_chapters: Int) {
|
|
||||||
|
|
||||||
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
|
||||||
remote_id = this@ALManga.id
|
|
||||||
title = title_romaji
|
|
||||||
total_chapters = this@ALManga.total_chapters
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
|
||||||
|
|
||||||
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
|
||||||
|
|
||||||
fun flatten() = lists.values.flatten()
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
|
||||||
|
|
||||||
data class ALUserManga(
|
|
||||||
val id: Int,
|
|
||||||
val list_status: String,
|
|
||||||
val score_raw: Int,
|
|
||||||
val chapters_read: Int,
|
|
||||||
val manga: ALManga) {
|
|
||||||
|
|
||||||
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
|
||||||
remote_id = manga.id
|
|
||||||
status = getMangaSyncStatus()
|
|
||||||
score = score_raw.toFloat()
|
|
||||||
last_chapter_read = chapters_read
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMangaSyncStatus() = when (list_status) {
|
|
||||||
"reading" -> Anilist.READING
|
|
||||||
"completed" -> Anilist.COMPLETED
|
|
||||||
"on-hold" -> Anilist.ON_HOLD
|
|
||||||
"dropped" -> Anilist.DROPPED
|
|
||||||
"plan to read" -> Anilist.PLAN_TO_READ
|
|
||||||
else -> throw NotImplementedError("Unknown status")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,222 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.myanimelist
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Xml
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
|
||||||
import eu.kanade.tachiyomi.data.network.asObservable
|
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
|
||||||
import okhttp3.Credentials
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.xmlpull.v1.XmlSerializer
|
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import java.io.StringWriter
|
|
||||||
|
|
||||||
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
|
||||||
|
|
||||||
private lateinit var headers: Headers
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val BASE_URL = "https://myanimelist.net"
|
|
||||||
|
|
||||||
private val ENTRY_TAG = "entry"
|
|
||||||
private val CHAPTER_TAG = "chapter"
|
|
||||||
private val SCORE_TAG = "score"
|
|
||||||
private val STATUS_TAG = "status"
|
|
||||||
|
|
||||||
val READING = 1
|
|
||||||
val COMPLETED = 2
|
|
||||||
val ON_HOLD = 3
|
|
||||||
val DROPPED = 4
|
|
||||||
val PLAN_TO_READ = 6
|
|
||||||
|
|
||||||
val DEFAULT_STATUS = READING
|
|
||||||
val DEFAULT_SCORE = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val username = getUsername()
|
|
||||||
val password = getPassword()
|
|
||||||
|
|
||||||
if (!username.isEmpty() && !password.isEmpty()) {
|
|
||||||
createHeaders(username, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name: String
|
|
||||||
get() = "MyAnimeList"
|
|
||||||
|
|
||||||
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/account/verify_credentials.xml")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/manga/search.xml")
|
|
||||||
.appendQueryParameter("q", query)
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendPath("malappinfo.php")
|
|
||||||
.appendQueryParameter("u", username)
|
|
||||||
.appendQueryParameter("status", "all")
|
|
||||||
.appendQueryParameter("type", "manga")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/mangalist/update")
|
|
||||||
.appendPath("${manga.remote_id}.xml")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/mangalist/add")
|
|
||||||
.appendPath("${manga.remote_id}.xml")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
override fun login(username: String, password: String): Completable {
|
|
||||||
createHeaders(username, password)
|
|
||||||
return client.newCall(GET(getLoginUrl(), headers))
|
|
||||||
.asObservable()
|
|
||||||
.doOnNext { it.close() }
|
|
||||||
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
|
|
||||||
.toCompletable()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(query: String): Observable<List<MangaSync>> {
|
|
||||||
return client.newCall(GET(getSearchUrl(query), headers))
|
|
||||||
.asObservable()
|
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
|
||||||
.flatMap { Observable.from(it.select("entry")) }
|
|
||||||
.filter { it.select("type").text() != "Novel" }
|
|
||||||
.map {
|
|
||||||
MangaSync.create(id).apply {
|
|
||||||
title = it.selectText("title")!!
|
|
||||||
remote_id = it.selectInt("id")
|
|
||||||
total_chapters = it.selectInt("chapters")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAL doesn't support score with decimals
|
|
||||||
fun getList(): Observable<List<MangaSync>> {
|
|
||||||
return networkService.forceCacheClient
|
|
||||||
.newCall(GET(getListUrl(getUsername()), headers))
|
|
||||||
.asObservable()
|
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
|
||||||
.flatMap { Observable.from(it.select("manga")) }
|
|
||||||
.map {
|
|
||||||
MangaSync.create(id).apply {
|
|
||||||
title = it.selectText("series_title")!!
|
|
||||||
remote_id = it.selectInt("series_mangadb_id")
|
|
||||||
last_chapter_read = it.selectInt("my_read_chapters")
|
|
||||||
status = it.selectInt("my_status")
|
|
||||||
score = it.selectInt("my_score").toFloat()
|
|
||||||
total_chapters = it.selectInt("series_chapters")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
return Observable.defer {
|
|
||||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
|
||||||
manga.status = COMPLETED
|
|
||||||
}
|
|
||||||
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
|
|
||||||
.asObservable()
|
|
||||||
.doOnNext { it.close() }
|
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
return Observable.defer {
|
|
||||||
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
|
|
||||||
.asObservable()
|
|
||||||
.doOnNext { it.close() }
|
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
|
|
||||||
val xml = Xml.newSerializer()
|
|
||||||
val writer = StringWriter()
|
|
||||||
|
|
||||||
with(xml) {
|
|
||||||
setOutput(writer)
|
|
||||||
startDocument("UTF-8", false)
|
|
||||||
startTag("", ENTRY_TAG)
|
|
||||||
|
|
||||||
// Last chapter read
|
|
||||||
if (manga.last_chapter_read != 0) {
|
|
||||||
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
|
|
||||||
}
|
|
||||||
// Manga status in the list
|
|
||||||
inTag(STATUS_TAG, manga.status.toString())
|
|
||||||
|
|
||||||
// Manga score
|
|
||||||
inTag(SCORE_TAG, manga.score.toString())
|
|
||||||
|
|
||||||
endTag("", ENTRY_TAG)
|
|
||||||
endDocument()
|
|
||||||
}
|
|
||||||
|
|
||||||
val form = FormBody.Builder()
|
|
||||||
form.add("data", writer.toString())
|
|
||||||
return form.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
|
||||||
startTag(namespace, tag)
|
|
||||||
text(body)
|
|
||||||
endTag(namespace, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
return getList()
|
|
||||||
.flatMap { userlist ->
|
|
||||||
manga.sync_id = id
|
|
||||||
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
|
||||||
if (mangaFromList != null) {
|
|
||||||
manga.copyPersonalFrom(mangaFromList)
|
|
||||||
update(manga)
|
|
||||||
} else {
|
|
||||||
// Set default fields if it's not found in the list
|
|
||||||
manga.score = DEFAULT_SCORE.toFloat()
|
|
||||||
manga.status = DEFAULT_STATUS
|
|
||||||
add(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
|
||||||
when (status) {
|
|
||||||
READING -> getString(R.string.reading)
|
|
||||||
COMPLETED -> getString(R.string.completed)
|
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
|
||||||
DROPPED -> getString(R.string.dropped)
|
|
||||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createHeaders(username: String, password: String) {
|
|
||||||
val builder = Headers.Builder()
|
|
||||||
builder.add("Authorization", Credentials.basic(username, password))
|
|
||||||
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
|
||||||
headers = builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -46,6 +46,15 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Call.asObservableSuccess(): Observable<Response> {
|
||||||
|
return asObservable().doOnNext { response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
response.close()
|
||||||
|
throw Exception("HTTP error ${response.code()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
val progressClient = newBuilder()
|
val progressClient = newBuilder()
|
||||||
.cache(null)
|
.cache(null)
|
||||||
|
@ -51,9 +51,9 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
|
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
|
||||||
|
|
||||||
val autoUpdateMangaSync = context.getString(R.string.pref_auto_update_manga_sync_key)
|
val autoUpdateTrack = context.getString(R.string.pref_auto_update_manga_sync_key)
|
||||||
|
|
||||||
val askUpdateMangaSync = context.getString(R.string.pref_ask_update_manga_sync_key)
|
val askUpdateTrack = context.getString(R.string.pref_ask_update_manga_sync_key)
|
||||||
|
|
||||||
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
||||||
|
|
||||||
@ -83,18 +83,26 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val filterUnread = context.getString(R.string.pref_filter_unread_key)
|
val filterUnread = context.getString(R.string.pref_filter_unread_key)
|
||||||
|
|
||||||
|
val librarySortingMode = context.getString(R.string.pref_library_sorting_mode_key)
|
||||||
|
|
||||||
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
|
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
|
||||||
|
|
||||||
val startScreen = context.getString(R.string.pref_start_screen_key)
|
val startScreen = context.getString(R.string.pref_start_screen_key)
|
||||||
|
|
||||||
|
val downloadNew = context.getString(R.string.pref_download_new_key)
|
||||||
|
|
||||||
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
|
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
||||||
|
|
||||||
fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
|
||||||
|
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||||
|
|
||||||
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
||||||
|
|
||||||
|
val lang = context.getString(R.string.pref_language_key)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,15 @@ import android.preference.PreferenceManager
|
|||||||
import com.f2prateek.rx.preferences.Preference
|
import com.f2prateek.rx.preferences.Preference
|
||||||
import com.f2prateek.rx.preferences.RxSharedPreferences
|
import com.f2prateek.rx.preferences.RxSharedPreferences
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||||
|
|
||||||
class PreferencesHelper(context: Context) {
|
fun Preference<Boolean>.invert(): Boolean = getOrDefault().let { set(!it); !it }
|
||||||
|
|
||||||
|
class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
val keys = PreferenceKeys(context)
|
val keys = PreferenceKeys(context)
|
||||||
|
|
||||||
@ -68,9 +70,9 @@ class PreferencesHelper(context: Context) {
|
|||||||
|
|
||||||
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
|
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
|
||||||
|
|
||||||
fun autoUpdateMangaSync() = prefs.getBoolean(keys.autoUpdateMangaSync, true)
|
fun autoUpdateTrack() = prefs.getBoolean(keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun askUpdateMangaSync() = prefs.getBoolean(keys.askUpdateMangaSync, false)
|
fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
|
||||||
|
|
||||||
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
|
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
|
||||||
|
|
||||||
@ -80,7 +82,7 @@ class PreferencesHelper(context: Context) {
|
|||||||
|
|
||||||
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
||||||
|
|
||||||
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN"))
|
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("en"))
|
||||||
|
|
||||||
fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "")
|
fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "")
|
||||||
|
|
||||||
@ -93,17 +95,21 @@ class PreferencesHelper(context: Context) {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mangaSyncUsername(sync: MangaSyncService) = prefs.getString(keys.syncUsername(sync.id), "")
|
fun trackUsername(sync: TrackService) = prefs.getString(keys.trackUsername(sync.id), "")
|
||||||
|
|
||||||
fun mangaSyncPassword(sync: MangaSyncService) = prefs.getString(keys.syncPassword(sync.id), "")
|
fun trackPassword(sync: TrackService) = prefs.getString(keys.trackPassword(sync.id), "")
|
||||||
|
|
||||||
fun setMangaSyncCredentials(sync: MangaSyncService, username: String, password: String) {
|
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(keys.syncUsername(sync.id), username)
|
.putString(keys.trackUsername(sync.id), username)
|
||||||
.putString(keys.syncPassword(sync.id), password)
|
.putString(keys.trackPassword(sync.id), password)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun trackToken(sync: TrackService) = rxPrefs.getString(keys.trackToken(sync.id), "")
|
||||||
|
|
||||||
|
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
||||||
|
|
||||||
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||||
|
|
||||||
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
||||||
@ -126,8 +132,16 @@ class PreferencesHelper(context: Context) {
|
|||||||
|
|
||||||
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
|
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
|
||||||
|
|
||||||
|
fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0)
|
||||||
|
|
||||||
|
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
|
||||||
|
|
||||||
fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
|
fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
|
||||||
|
|
||||||
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
||||||
|
|
||||||
|
fun downloadNew() = prefs.getBoolean(keys.downloadNew, false)
|
||||||
|
|
||||||
|
fun lang() = prefs.getString(keys.lang, "")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source
|
|
||||||
|
|
||||||
class Language(val code: String, val lang: String)
|
|
||||||
|
|
||||||
val DE = Language("DE", "German")
|
|
||||||
val EN = Language("EN", "English")
|
|
||||||
val RU = Language("RU", "Russian")
|
|
||||||
|
|
||||||
fun getLanguages() = listOf(DE, EN, RU)
|
|
@ -6,10 +6,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.data.network.asObservable
|
import eu.kanade.tachiyomi.data.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
@ -47,9 +46,9 @@ abstract class OnlineSource() : Source {
|
|||||||
abstract val baseUrl: String
|
abstract val baseUrl: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Language of the source.
|
* An ISO 639-1 compliant language code (two characters in lower case).
|
||||||
*/
|
*/
|
||||||
abstract val lang: Language
|
abstract val lang: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the source has support for latest updates.
|
* Whether the source has support for latest updates.
|
||||||
@ -82,7 +81,7 @@ abstract class OnlineSource() : Source {
|
|||||||
/**
|
/**
|
||||||
* Visible name of the source.
|
* Visible name of the source.
|
||||||
*/
|
*/
|
||||||
override fun toString() = "$name (${lang.code})"
|
override fun toString() = "$name (${lang.toUpperCase()})"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
@ -93,7 +92,7 @@ abstract class OnlineSource() : Source {
|
|||||||
*/
|
*/
|
||||||
open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = client
|
open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = client
|
||||||
.newCall(popularMangaRequest(page))
|
.newCall(popularMangaRequest(page))
|
||||||
.asObservable()
|
.asObservableSuccess()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
popularMangaParse(response, page)
|
popularMangaParse(response, page)
|
||||||
page
|
page
|
||||||
@ -136,7 +135,7 @@ abstract class OnlineSource() : Source {
|
|||||||
*/
|
*/
|
||||||
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
|
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
|
||||||
.newCall(searchMangaRequest(page, query, filters))
|
.newCall(searchMangaRequest(page, query, filters))
|
||||||
.asObservable()
|
.asObservableSuccess()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
searchMangaParse(response, page, query, filters)
|
searchMangaParse(response, page, query, filters)
|
||||||
page
|
page
|
||||||
@ -178,7 +177,7 @@ abstract class OnlineSource() : Source {
|
|||||||
*/
|
*/
|
||||||
open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = client
|
open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = client
|
||||||
.newCall(latestUpdatesRequest(page))
|
.newCall(latestUpdatesRequest(page))
|
||||||
.asObservable()
|
.asObservableSuccess()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
latestUpdatesParse(response, page)
|
latestUpdatesParse(response, page)
|
||||||
page
|
page
|
||||||
@ -212,7 +211,7 @@ abstract class OnlineSource() : Source {
|
|||||||
*/
|
*/
|
||||||
override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client
|
override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client
|
||||||
.newCall(mangaDetailsRequest(manga))
|
.newCall(mangaDetailsRequest(manga))
|
||||||
.asObservable()
|
.asObservableSuccess()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
Manga.create(manga.url, id).apply {
|
Manga.create(manga.url, id).apply {
|
||||||
mangaDetailsParse(response, this)
|
mangaDetailsParse(response, this)
|
||||||
@ -246,7 +245,7 @@ abstract class OnlineSource() : Source {
|
|||||||
*/
|
*/
|
||||||
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> = client
|
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> = client
|
||||||
.newCall(chapterListRequest(manga))
|
.newCall(chapterListRequest(manga))
|
||||||
.asObservable()
|
.asObservableSuccess()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
mutableListOf<Chapter>().apply {
|
mutableListOf<Chapter>().apply {
|
||||||
chapterListParse(response, this)
|
chapterListParse(response, this)
|
||||||
@ -292,11 +291,8 @@ abstract class OnlineSource() : Source {
|
|||||||
*/
|
*/
|
||||||
open fun fetchPageListFromNetwork(chapter: Chapter): Observable<List<Page>> = client
|
open fun fetchPageListFromNetwork(chapter: Chapter): Observable<List<Page>> = client
|
||||||
.newCall(pageListRequest(chapter))
|
.newCall(pageListRequest(chapter))
|
||||||
.asObservable()
|
.asObservableSuccess()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
if (!response.isSuccessful) {
|
|
||||||
throw Exception("Webpage sent ${response.code()} code")
|
|
||||||
}
|
|
||||||
mutableListOf<Page>().apply {
|
mutableListOf<Page>().apply {
|
||||||
pageListParse(response, this)
|
pageListParse(response, this)
|
||||||
if (isEmpty()) {
|
if (isEmpty()) {
|
||||||
@ -338,7 +334,7 @@ abstract class OnlineSource() : Source {
|
|||||||
page.status = Page.LOAD_PAGE
|
page.status = Page.LOAD_PAGE
|
||||||
return client
|
return client
|
||||||
.newCall(imageUrlRequest(page))
|
.newCall(imageUrlRequest(page))
|
||||||
.asObservable()
|
.asObservableSuccess()
|
||||||
.map { imageUrlParse(it) }
|
.map { imageUrlParse(it) }
|
||||||
.doOnError { page.status = Page.ERROR }
|
.doOnError { page.status = Page.ERROR }
|
||||||
.onErrorReturn { null }
|
.onErrorReturn { null }
|
||||||
@ -381,13 +377,7 @@ abstract class OnlineSource() : Source {
|
|||||||
*/
|
*/
|
||||||
fun imageResponse(page: Page): Observable<Response> = client
|
fun imageResponse(page: Page): Observable<Response> = client
|
||||||
.newCallWithProgress(imageRequest(page), page)
|
.newCallWithProgress(imageRequest(page), page)
|
||||||
.asObservable()
|
.asObservableSuccess()
|
||||||
.doOnNext {
|
|
||||||
if (!it.isSuccessful) {
|
|
||||||
it.close()
|
|
||||||
throw RuntimeException("Not a valid response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the request for getting the source image. Override only if it's needed to override
|
* Returns the request for getting the source image. Override only if it's needed to override
|
||||||
|
@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
import eu.kanade.tachiyomi.data.source.getLanguages
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@ -27,9 +26,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
|||||||
if (it.endsWith("/")) it.dropLast(1) else it
|
if (it.endsWith("/")) it.dropLast(1) else it
|
||||||
}
|
}
|
||||||
|
|
||||||
override val lang = map.lang.toUpperCase().let { code ->
|
override val lang = map.lang.toLowerCase()
|
||||||
getLanguages().find { code == it.code }!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override val supportsLatest = map.latestupdates != null
|
override val supportsLatest = map.latestupdates != null
|
||||||
|
|
||||||
@ -39,7 +36,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val id = map.id.let {
|
override val id = map.id.let {
|
||||||
if (it is Int) it else (lang.code.hashCode() + 31 * it.hashCode()) and 0x7fffffff
|
if (it is Int) it else (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaRequest(page: MangasPage): Request {
|
override fun popularMangaRequest(page: MangasPage): Request {
|
||||||
|
@ -7,8 +7,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
import eu.kanade.tachiyomi.data.network.asObservable
|
import eu.kanade.tachiyomi.data.network.asObservable
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||||
@ -33,7 +31,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
|||||||
|
|
||||||
override val baseUrl = "http://bato.to"
|
override val baseUrl = "http://bato.to"
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang = "en"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
@ -109,12 +107,17 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
|||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1&genre_cond=and&genres=${getFilterParams(filters)}"
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1${getFilterParams(filters)}"
|
||||||
|
|
||||||
private fun getFilterParams(filters: List<Filter>): String = filters
|
private fun getFilterParams(filters: List<Filter>): String {
|
||||||
.map {
|
var genres = ""
|
||||||
";i" + it.id
|
var completed = ""
|
||||||
}.joinToString()
|
for (filter in filters) {
|
||||||
|
if (filter.equals(completedFilter)) completed = "&completed=c"
|
||||||
|
else genres += ";i" + filter.id
|
||||||
|
}
|
||||||
|
return if (genres.isEmpty()) completed else "&genres=$genres&genre_cond=and$completed"
|
||||||
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
@ -133,7 +136,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
||||||
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}&order_cond=views&order=desc&genre_cond=and&genres=" + getFilterParams(filters)
|
"$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=${page.page + 1}${getFilterParams(filters)}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,11 +304,13 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val completedFilter = Filter("completed", "Completed")
|
||||||
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
||||||
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Filter("${id}", "${el.textContent.trim()}")`
|
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Filter("${id}", "${el.textContent.trim()}")`
|
||||||
// }).join(',\n')
|
// }).join(',\n')
|
||||||
// on https://bato.to/search
|
// on https://bato.to/search
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
completedFilter,
|
||||||
Filter("40", "4-Koma"),
|
Filter("40", "4-Koma"),
|
||||||
Filter("1", "Action"),
|
Filter("1", "Action"),
|
||||||
Filter("2", "Adventure"),
|
Filter("2", "Adventure"),
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
@ -25,7 +22,7 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override val baseUrl = "http://kissmanga.com"
|
override val baseUrl = "http://kissmanga.com"
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang = "en"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
@ -62,10 +59,10 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
|||||||
val form = FormBody.Builder().apply {
|
val form = FormBody.Builder().apply {
|
||||||
add("authorArtist", "")
|
add("authorArtist", "")
|
||||||
add("mangaName", query)
|
add("mangaName", query)
|
||||||
add("status", "")
|
|
||||||
|
|
||||||
this@Kissmanga.filters.forEach { filter ->
|
this@Kissmanga.filters.forEach { filter ->
|
||||||
add("genres", if (filter in filters) "1" else "0")
|
if (filter.equals(completedFilter)) add("status", if (filter in filters) filter.id else "")
|
||||||
|
else add("genres", if (filter in filters) "1" else "0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,9 +128,11 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
private val completedFilter = Filter("Completed", "Completed")
|
||||||
// $("select[name=\"genres\"]").map((i,el) => `Filter("${i}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
// $("select[name=\"genres\"]").map((i,el) => `Filter("${i}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
||||||
// on http://kissmanga.com/AdvanceSearch
|
// on http://kissmanga.com/AdvanceSearch
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
completedFilter,
|
||||||
Filter("0", "Action"),
|
Filter("0", "Action"),
|
||||||
Filter("1", "Adult"),
|
Filter("1", "Adult"),
|
||||||
Filter("2", "Adventure"),
|
Filter("2", "Adventure"),
|
||||||
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.source.online.english
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@ -20,7 +18,7 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override val baseUrl = "http://mangafox.me"
|
override val baseUrl = "http://mangafox.me"
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang = "en"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
@ -50,10 +48,10 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
|||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}"
|
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}"
|
||||||
|
|
||||||
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
|
override fun searchMangaSelector() = "div#mangalist > ul.list > li"
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("a.series_preview").first().let {
|
element.select("a.title").first().let {
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text()
|
manga.title = it.text()
|
||||||
}
|
}
|
||||||
@ -132,6 +130,7 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
|||||||
// $('select.genres').map((i,el)=>`Filter("${$(el).attr('name')}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
// $('select.genres').map((i,el)=>`Filter("${$(el).attr('name')}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
||||||
// on http://kissmanga.com/AdvanceSearch
|
// on http://kissmanga.com/AdvanceSearch
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("is_completed", "Completed"),
|
||||||
Filter("genres[Action]", "Action"),
|
Filter("genres[Action]", "Action"),
|
||||||
Filter("genres[Adult]", "Adult"),
|
Filter("genres[Adult]", "Adult"),
|
||||||
Filter("genres[Adventure]", "Adventure"),
|
Filter("genres[Adventure]", "Adventure"),
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
@ -19,7 +16,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override val baseUrl = "http://www.mangahere.co"
|
override val baseUrl = "http://www.mangahere.co"
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang = "en"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
@ -31,13 +28,17 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
private fun mangaFromElement(query: String, element: Element, manga: Manga) {
|
||||||
element.select("div.title > a").first().let {
|
element.select(query).first().let {
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text()
|
manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
mangaFromElement("div.title > a", element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
popularMangaFromElement(element, manga)
|
popularMangaFromElement(element, manga)
|
||||||
}
|
}
|
||||||
@ -51,10 +52,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
|||||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("a.manga_info").first().let {
|
mangaFromElement("a.manga_info", element, manga)
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||||
@ -136,6 +134,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
|||||||
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Filter("${el.getAttribute('name')}", "${el.nextSibling.nextSibling.textContent.trim()}")`).join(',\n')
|
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Filter("${el.getAttribute('name')}", "${el.nextSibling.nextSibling.textContent.trim()}")`).join(',\n')
|
||||||
// http://www.mangahere.co/advsearch.htm
|
// http://www.mangahere.co/advsearch.htm
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("is_completed", "Completed"),
|
||||||
Filter("genres[Action]", "Action"),
|
Filter("genres[Action]", "Action"),
|
||||||
Filter("genres[Adventure]", "Adventure"),
|
Filter("genres[Adventure]", "Adventure"),
|
||||||
Filter("genres[Comedy]", "Comedy"),
|
Filter("genres[Comedy]", "Comedy"),
|
||||||
|
@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.data.source.online.english
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
@ -24,7 +22,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override val baseUrl = "http://mangaseeonline.net"
|
override val baseUrl = "http://mangaseeonline.net"
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang = "en"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
@ -66,8 +64,16 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
|||||||
// Not used, overrides parent.
|
// Not used, overrides parent.
|
||||||
override fun popularMangaNextPageSelector() = ""
|
override fun popularMangaNextPageSelector() = ""
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>): String {
|
||||||
"$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&keyword=$query&genre=${filters.map { it.id }.joinToString(",")}"
|
var url = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&keyword=$query"
|
||||||
|
var genres: String? = null
|
||||||
|
for (filter in filters) {
|
||||||
|
if (filter.equals(completedFilter)) url += "&status=Complete"
|
||||||
|
else if (genres == null) genres = filter.id
|
||||||
|
else genres += "," + filter.id
|
||||||
|
}
|
||||||
|
return if (genres == null) url else url + "&genre=$genres"
|
||||||
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.searchResults > div.requested > div.row"
|
override fun searchMangaSelector() = "div.searchResults > div.requested > div.row"
|
||||||
|
|
||||||
@ -168,9 +174,11 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
||||||
|
|
||||||
|
private val completedFilter = Filter("Complete", "Completed")
|
||||||
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||||
// http://mangasee.co/advanced-search/
|
// http://mangasee.co/advanced-search/
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
completedFilter,
|
||||||
Filter("Action", "Action"),
|
Filter("Action", "Action"),
|
||||||
Filter("Adult", "Adult"),
|
Filter("Adult", "Adult"),
|
||||||
Filter("Adventure", "Adventure"),
|
Filter("Adventure", "Adventure"),
|
||||||
@ -250,4 +258,4 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,11 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
@ -23,7 +20,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override val baseUrl = "http://www.readmanga.today"
|
override val baseUrl = "http://www.readmanga.today"
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang = "en"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
@ -72,10 +69,12 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
|||||||
val builder = okhttp3.FormBody.Builder()
|
val builder = okhttp3.FormBody.Builder()
|
||||||
builder.add("manga-name", query)
|
builder.add("manga-name", query)
|
||||||
builder.add("type", "all")
|
builder.add("type", "all")
|
||||||
builder.add("status", "both")
|
var status = "both"
|
||||||
for (filter in filters) {
|
for (filter in filters) {
|
||||||
builder.add("include[]", filter.id)
|
if (filter.equals(completedFilter)) status = filter.id
|
||||||
|
else builder.add("include[]", filter.id)
|
||||||
}
|
}
|
||||||
|
builder.add("status", status)
|
||||||
|
|
||||||
return POST(page.url, headers, builder.build())
|
return POST(page.url, headers, builder.build())
|
||||||
}
|
}
|
||||||
@ -154,9 +153,11 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
||||||
|
|
||||||
|
private val completedFilter = Filter("completed", "Completed")
|
||||||
// [...document.querySelectorAll("ul.manga-cat span")].map(el => `Filter("${el.getAttribute('data-id')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
// [...document.querySelectorAll("ul.manga-cat span")].map(el => `Filter("${el.getAttribute('data-id')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||||
// http://www.readmanga.today/advanced-search
|
// http://www.readmanga.today/advanced-search
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
completedFilter,
|
||||||
Filter("2", "Action"),
|
Filter("2", "Action"),
|
||||||
Filter("4", "Adventure"),
|
Filter("4", "Adventure"),
|
||||||
Filter("5", "Comedy"),
|
Filter("5", "Comedy"),
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.german
|
package eu.kanade.tachiyomi.data.source.online.german
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.DE
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@ -19,7 +16,7 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override val baseUrl = "http://www.wiemanga.com"
|
override val baseUrl = "http://www.wiemanga.com"
|
||||||
|
|
||||||
override val lang: Language get() = DE
|
override val lang = "de"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.source.online.russian
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.RU
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
@ -20,7 +18,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override val baseUrl = "http://mangachan.me"
|
override val baseUrl = "http://mangachan.me"
|
||||||
|
|
||||||
override val lang: Language get() = RU
|
override val lang = "ru"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.source.online.russian
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.RU
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -19,7 +17,7 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override val baseUrl = "http://mintmanga.com"
|
override val baseUrl = "http://mintmanga.com"
|
||||||
|
|
||||||
override val lang: Language get() = RU
|
override val lang = "ru"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.source.online.russian
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.RU
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -19,7 +17,7 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
|
|||||||
|
|
||||||
override val baseUrl = "http://readmanga.me"
|
override val baseUrl = "http://readmanga.me"
|
||||||
|
|
||||||
override val lang: Language get() = RU
|
override val lang = "ru"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
|
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
||||||
|
|
||||||
|
class TrackManager(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MYANIMELIST = 1
|
||||||
|
const val ANILIST = 2
|
||||||
|
const val KITSU = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
||||||
|
|
||||||
|
val aniList = Anilist(context, ANILIST)
|
||||||
|
|
||||||
|
val kitsu = Kitsu(context, KITSU)
|
||||||
|
|
||||||
|
val services = listOf(myAnimeList, aniList, kitsu)
|
||||||
|
|
||||||
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
|
fun hasLoggedServices() = services.any { it.isLogged }
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
import android.support.annotation.CallSuper
|
||||||
|
import android.support.annotation.DrawableRes
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class TrackService(val id: Int) {
|
||||||
|
|
||||||
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
val networkService: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
open val client: OkHttpClient
|
||||||
|
get() = networkService.client
|
||||||
|
|
||||||
|
// Name of the manga sync service to display
|
||||||
|
abstract val name: String
|
||||||
|
|
||||||
|
@DrawableRes
|
||||||
|
abstract fun getLogo(): Int
|
||||||
|
|
||||||
|
abstract fun getLogoColor(): Int
|
||||||
|
|
||||||
|
abstract fun getStatusList(): List<Int>
|
||||||
|
|
||||||
|
abstract fun getStatus(status: Int): String
|
||||||
|
|
||||||
|
abstract fun getScoreList(): List<String>
|
||||||
|
|
||||||
|
open fun indexToScore(index: Int): Float {
|
||||||
|
return index.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun displayScore(track: Track): String
|
||||||
|
|
||||||
|
abstract fun add(track: Track): Observable<Track>
|
||||||
|
|
||||||
|
abstract fun update(track: Track): Observable<Track>
|
||||||
|
|
||||||
|
abstract fun bind(track: Track): Observable<Track>
|
||||||
|
|
||||||
|
abstract fun search(query: String): Observable<List<Track>>
|
||||||
|
|
||||||
|
abstract fun refresh(track: Track): Observable<Track>
|
||||||
|
|
||||||
|
abstract fun login(username: String, password: String): Completable
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
open fun logout() {
|
||||||
|
preferences.setTrackCredentials(this, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
open val isLogged: Boolean
|
||||||
|
get() = !getUsername().isEmpty() &&
|
||||||
|
!getPassword().isEmpty()
|
||||||
|
|
||||||
|
fun getUsername() = preferences.trackUsername(this)
|
||||||
|
|
||||||
|
fun getPassword() = preferences.trackPassword(this)
|
||||||
|
|
||||||
|
fun saveCredentials(username: String, password: String) {
|
||||||
|
preferences.setTrackCredentials(this, username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,20 +1,20 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class UpdateMangaSyncService : Service() {
|
class TrackUpdateService : Service() {
|
||||||
|
|
||||||
val syncManager: MangaSyncManager by injectLazy()
|
val trackManager: TrackManager by injectLazy()
|
||||||
val db: DatabaseHelper by injectLazy()
|
val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
private lateinit var subscriptions: CompositeSubscription
|
private lateinit var subscriptions: CompositeSubscription
|
||||||
@ -30,9 +30,9 @@ class UpdateMangaSyncService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
val manga = intent.getSerializableExtra(EXTRA_MANGASYNC)
|
val track = intent.getSerializableExtra(EXTRA_TRACK)
|
||||||
if (manga != null) {
|
if (track != null) {
|
||||||
updateLastChapterRead(manga as MangaSync, startId)
|
updateLastChapterRead(track as Track, startId)
|
||||||
return Service.START_REDELIVER_INTENT
|
return Service.START_REDELIVER_INTENT
|
||||||
} else {
|
} else {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
@ -44,15 +44,15 @@ class UpdateMangaSyncService : Service() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
|
private fun updateLastChapterRead(track: Track, startId: Int) {
|
||||||
val sync = syncManager.getService(mangaSync.sync_id)
|
val sync = trackManager.getService(track.sync_id)
|
||||||
if (sync == null) {
|
if (sync == null) {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions.add(Observable.defer { sync.update(mangaSync) }
|
subscriptions.add(Observable.defer { sync.update(track) }
|
||||||
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
|
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ stopSelf(startId) },
|
.subscribe({ stopSelf(startId) },
|
||||||
@ -61,12 +61,12 @@ class UpdateMangaSyncService : Service() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val EXTRA_MANGASYNC = "extra_mangasync"
|
private val EXTRA_TRACK = "extra_track"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun start(context: Context, mangaSync: MangaSync) {
|
fun start(context: Context, track: Track) {
|
||||||
val intent = Intent(context, UpdateMangaSyncService::class.java)
|
val intent = Intent(context, TrackUpdateService::class.java)
|
||||||
intent.putExtra(EXTRA_MANGASYNC, mangaSync)
|
intent.putExtra(EXTRA_TRACK, track)
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLAN_TO_READ = 5
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name = "AniList"
|
||||||
|
|
||||||
|
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
||||||
|
|
||||||
|
private val api by lazy { AnilistApi(client, interceptor) }
|
||||||
|
|
||||||
|
override fun getLogo() = R.drawable.al
|
||||||
|
|
||||||
|
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||||
|
|
||||||
|
override fun getStatusList(): List<Int> {
|
||||||
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getScoreList(): List<String> {
|
||||||
|
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||||
|
// 10 point
|
||||||
|
0 -> IntRange(0, 10).map(Int::toString)
|
||||||
|
// 100 point
|
||||||
|
1 -> IntRange(0, 100).map(Int::toString)
|
||||||
|
// 5 stars
|
||||||
|
2 -> IntRange(0, 5).map { "$it ★" }
|
||||||
|
// Smiley
|
||||||
|
3 -> listOf("-", "😦", "😐", "😊")
|
||||||
|
// 10 point decimal
|
||||||
|
4 -> IntRange(0, 100).map { (it / 10f).toString() }
|
||||||
|
else -> throw Exception("Unknown score type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun indexToScore(index: Int): Float {
|
||||||
|
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||||
|
// 10 point
|
||||||
|
0 -> index * 10f
|
||||||
|
// 100 point
|
||||||
|
1 -> index.toFloat()
|
||||||
|
// 5 stars
|
||||||
|
2 -> index * 20f
|
||||||
|
// Smiley
|
||||||
|
3 -> index * 30f
|
||||||
|
// 10 point decimal
|
||||||
|
4 -> index.toFloat()
|
||||||
|
else -> throw Exception("Unknown score type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun displayScore(track: Track): String {
|
||||||
|
val score = track.score
|
||||||
|
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||||
|
2 -> "${(score / 20).toInt()} ★"
|
||||||
|
3 -> when {
|
||||||
|
score == 0f -> "0"
|
||||||
|
score <= 30 -> "😦"
|
||||||
|
score <= 60 -> "😐"
|
||||||
|
else -> "😊"
|
||||||
|
}
|
||||||
|
else -> track.toAnilistScore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(track: Track): Observable<Track> {
|
||||||
|
return api.addLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(track: Track): Observable<Track> {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.updateLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
|
return api.findLibManga(track, getUsername())
|
||||||
|
.flatMap { remoteTrack ->
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
update(track)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun search(query: String): Observable<List<Track>> {
|
||||||
|
return api.search(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
|
return api.getLibManga(track, getUsername())
|
||||||
|
.map { remoteTrack ->
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
|
fun login(authCode: String): Completable {
|
||||||
|
return api.login(authCode)
|
||||||
|
// Save the token in the interceptor.
|
||||||
|
.doOnNext { interceptor.setAuth(it) }
|
||||||
|
// Obtain the authenticated user from the API.
|
||||||
|
.zipWith(api.getCurrentUser().map { pair ->
|
||||||
|
preferences.anilistScoreType().set(pair.second)
|
||||||
|
pair.first
|
||||||
|
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
|
||||||
|
// Save service credentials (username and refresh token).
|
||||||
|
.doOnNext { saveCredentials(it.first, it.second) }
|
||||||
|
// Logout on any error.
|
||||||
|
.doOnError { logout() }
|
||||||
|
.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
super.logout()
|
||||||
|
interceptor.setAuth(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.*
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
|
private val rest = restBuilder()
|
||||||
|
.client(client.newBuilder().addInterceptor(interceptor).build())
|
||||||
|
.build()
|
||||||
|
.create(Rest::class.java)
|
||||||
|
|
||||||
|
fun addLibManga(track: Track): Observable<Track> {
|
||||||
|
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
||||||
|
.map { response ->
|
||||||
|
response.body().close()
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw Exception("Could not add manga")
|
||||||
|
}
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLibManga(track: Track): Observable<Track> {
|
||||||
|
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
||||||
|
track.toAnilistScore())
|
||||||
|
.map { response ->
|
||||||
|
response.body().close()
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw Exception("Could not update manga")
|
||||||
|
}
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String): Observable<List<Track>> {
|
||||||
|
return rest.search(query, 1)
|
||||||
|
.map { list ->
|
||||||
|
list.filter { it.type != "Novel" }.map { it.toTrack() }
|
||||||
|
}
|
||||||
|
.onErrorReturn { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getList(username: String): Observable<List<Track>> {
|
||||||
|
return rest.getLib(username)
|
||||||
|
.map { lib ->
|
||||||
|
lib.flatten().map { it.toTrack() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findLibManga(track: Track, username: String) : Observable<Track?> {
|
||||||
|
// TODO avoid getting the entire list
|
||||||
|
return getList(username)
|
||||||
|
.map { list -> list.find { it.remote_id == track.remote_id } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibManga(track: Track, username: String): Observable<Track> {
|
||||||
|
return findLibManga(track, username)
|
||||||
|
.map { it ?: throw Exception("Could not find manga") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun login(authCode: String): Observable<OAuth> {
|
||||||
|
return restBuilder()
|
||||||
|
.client(client)
|
||||||
|
.build()
|
||||||
|
.create(Rest::class.java)
|
||||||
|
.requestAccessToken(authCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentUser(): Observable<Pair<String, Int>> {
|
||||||
|
return rest.getCurrentUser()
|
||||||
|
.map { it["id"].string to it["score_type"].int }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restBuilder() = Retrofit.Builder()
|
||||||
|
.baseUrl(baseUrl)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
|
|
||||||
|
private interface Rest {
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("auth/access_token")
|
||||||
|
fun requestAccessToken(
|
||||||
|
@Field("code") code: String,
|
||||||
|
@Field("grant_type") grant_type: String = "authorization_code",
|
||||||
|
@Field("client_id") client_id: String = clientId,
|
||||||
|
@Field("client_secret") client_secret: String = clientSecret,
|
||||||
|
@Field("redirect_uri") redirect_uri: String = clientUrl
|
||||||
|
) : Observable<OAuth>
|
||||||
|
|
||||||
|
@GET("user")
|
||||||
|
fun getCurrentUser(): Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("manga/search/{query}")
|
||||||
|
fun search(
|
||||||
|
@Path("query") query: String,
|
||||||
|
@Query("page") page: Int
|
||||||
|
): Observable<List<ALManga>>
|
||||||
|
|
||||||
|
@GET("user/{username}/mangalist")
|
||||||
|
fun getLib(
|
||||||
|
@Path("username") username: String
|
||||||
|
): Observable<ALUserLists>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@PUT("mangalist")
|
||||||
|
fun addLibManga(
|
||||||
|
@Field("id") id: Int,
|
||||||
|
@Field("chapters_read") chapters_read: Int,
|
||||||
|
@Field("list_status") list_status: String
|
||||||
|
) : Observable<Response<ResponseBody>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@PUT("mangalist")
|
||||||
|
fun updateLibManga(
|
||||||
|
@Field("id") id: Int,
|
||||||
|
@Field("chapters_read") chapters_read: Int,
|
||||||
|
@Field("list_status") list_status: String,
|
||||||
|
@Field("score") score_raw: String
|
||||||
|
) : Observable<Response<ResponseBody>>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "tachiyomi-hrtje"
|
||||||
|
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
||||||
|
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||||
|
private const val baseUrl = "https://anilist.co/api/"
|
||||||
|
|
||||||
|
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
||||||
|
.appendQueryParameter("grant_type", "authorization_code")
|
||||||
|
.appendQueryParameter("client_id", clientId)
|
||||||
|
.appendQueryParameter("redirect_uri", clientUrl)
|
||||||
|
.appendQueryParameter("response_type", "code")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.build())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,61 +1,60 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Response
|
||||||
import okhttp3.Response
|
|
||||||
|
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||||
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
|
||||||
|
/**
|
||||||
/**
|
* OAuth object used for authenticated requests.
|
||||||
* OAuth object used for authenticated requests.
|
*
|
||||||
*
|
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
* before its original expiration date.
|
||||||
* before its original expiration date.
|
*/
|
||||||
*/
|
private var oauth: OAuth? = null
|
||||||
private var oauth: OAuth? = null
|
set(value) {
|
||||||
set(value) {
|
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
}
|
||||||
}
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
val originalRequest = chain.request()
|
||||||
val originalRequest = chain.request()
|
|
||||||
|
if (refreshToken.isNullOrEmpty()) {
|
||||||
if (refreshToken.isNullOrEmpty()) {
|
throw Exception("Not authenticated with Anilist")
|
||||||
throw Exception("Not authenticated with Anilist")
|
}
|
||||||
}
|
|
||||||
|
// Refresh access token if null or expired.
|
||||||
// Refresh access token if null or expired.
|
if (oauth == null || oauth!!.isExpired()) {
|
||||||
if (oauth == null || oauth!!.isExpired()) {
|
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
oauth = if (response.isSuccessful) {
|
||||||
oauth = if (response.isSuccessful) {
|
Gson().fromJson(response.body().string(), OAuth::class.java)
|
||||||
Gson().fromJson(response.body().string(), OAuth::class.java)
|
} else {
|
||||||
} else {
|
response.close()
|
||||||
response.close()
|
null
|
||||||
null
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Throw on null auth.
|
||||||
// Throw on null auth.
|
if (oauth == null) {
|
||||||
if (oauth == null) {
|
throw Exception("Access token wasn't refreshed")
|
||||||
throw Exception("Access token wasn't refreshed")
|
}
|
||||||
}
|
|
||||||
|
// Add the authorization header to the original request.
|
||||||
// Add the authorization header to the original request.
|
val authRequest = originalRequest.newBuilder()
|
||||||
val authRequest = originalRequest.newBuilder()
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
.build()
|
||||||
.build()
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
return chain.proceed(authRequest)
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||||
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
* and the oauth object.
|
||||||
* and the oauth object.
|
*/
|
||||||
*/
|
fun setAuth(oauth: OAuth?) {
|
||||||
fun setAuth(oauth: OAuth?) {
|
refreshToken = oauth?.refresh_token
|
||||||
refreshToken = oauth?.refresh_token
|
this.oauth = oauth
|
||||||
this.oauth = oauth
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
data class ALManga(
|
||||||
|
val id: Int,
|
||||||
|
val title_romaji: String,
|
||||||
|
val type: String,
|
||||||
|
val total_chapters: Int) {
|
||||||
|
|
||||||
|
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||||
|
remote_id = this@ALManga.id
|
||||||
|
title = title_romaji
|
||||||
|
total_chapters = this@ALManga.total_chapters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ALUserManga(
|
||||||
|
val id: Int,
|
||||||
|
val list_status: String,
|
||||||
|
val score_raw: Int,
|
||||||
|
val chapters_read: Int,
|
||||||
|
val manga: ALManga) {
|
||||||
|
|
||||||
|
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||||
|
remote_id = manga.id
|
||||||
|
status = toTrackStatus()
|
||||||
|
score = score_raw.toFloat()
|
||||||
|
last_chapter_read = chapters_read
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toTrackStatus() = when (list_status) {
|
||||||
|
"reading" -> Anilist.READING
|
||||||
|
"completed" -> Anilist.COMPLETED
|
||||||
|
"on-hold" -> Anilist.ON_HOLD
|
||||||
|
"dropped" -> Anilist.DROPPED
|
||||||
|
"plan to read" -> Anilist.PLAN_TO_READ
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
||||||
|
|
||||||
|
fun flatten() = lists.values.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Track.toAnilistStatus() = when (status) {
|
||||||
|
Anilist.READING -> "reading"
|
||||||
|
Anilist.COMPLETED -> "completed"
|
||||||
|
Anilist.ON_HOLD -> "on-hold"
|
||||||
|
Anilist.DROPPED -> "dropped"
|
||||||
|
Anilist.PLAN_TO_READ -> "plan to read"
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
|
||||||
|
// 10 point
|
||||||
|
0 -> (score.toInt() / 10).toString()
|
||||||
|
// 100 point
|
||||||
|
1 -> score.toInt().toString()
|
||||||
|
// 5 stars
|
||||||
|
2 -> when {
|
||||||
|
score == 0f -> "0"
|
||||||
|
score < 30 -> "1"
|
||||||
|
score < 50 -> "2"
|
||||||
|
score < 70 -> "3"
|
||||||
|
score < 90 -> "4"
|
||||||
|
else -> "5"
|
||||||
|
}
|
||||||
|
// Smiley
|
||||||
|
3 -> when {
|
||||||
|
score == 0f -> "0"
|
||||||
|
score <= 30 -> ":("
|
||||||
|
score <= 60 -> ":|"
|
||||||
|
else -> ":)"
|
||||||
|
}
|
||||||
|
// 10 point decimal
|
||||||
|
4 -> (score / 10).toString()
|
||||||
|
else -> throw Exception("Unknown score type")
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
data class OAuth(
|
data class OAuth(
|
||||||
val access_token: String,
|
val access_token: String,
|
||||||
val token_type: String,
|
val token_type: String,
|
||||||
val expires: Long,
|
val expires: Long,
|
||||||
val expires_in: Long,
|
val expires_in: Long,
|
||||||
val refresh_token: String?) {
|
val refresh_token: String?) {
|
||||||
|
|
||||||
fun isExpired() = System.currentTimeMillis() > expires
|
fun isExpired() = System.currentTimeMillis() > expires
|
||||||
}
|
}
|
136
app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
Normal file
136
app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLAN_TO_READ = 5
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name = "Kitsu"
|
||||||
|
|
||||||
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
|
private val interceptor by lazy { KitsuInterceptor(this, gson) }
|
||||||
|
|
||||||
|
private val api by lazy { KitsuApi(client, interceptor) }
|
||||||
|
|
||||||
|
override fun getLogo(): Int {
|
||||||
|
return R.drawable.kitsu
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogoColor(): Int {
|
||||||
|
return Color.rgb(51, 37, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatusList(): List<Int> {
|
||||||
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getScoreList(): List<String> {
|
||||||
|
return IntRange(0, 10).map { (it.toFloat() / 2).toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun displayScore(track: Track): String {
|
||||||
|
return track.toKitsuScore()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(track: Track): Observable<Track> {
|
||||||
|
return api.addLibManga(track, getUserId())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(track: Track): Observable<Track> {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.updateLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
|
return api.findLibManga(track, getUserId())
|
||||||
|
.flatMap { remoteTrack ->
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.remote_id = remoteTrack.remote_id
|
||||||
|
update(track)
|
||||||
|
} else {
|
||||||
|
track.score = DEFAULT_SCORE
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun search(query: String): Observable<List<Track>> {
|
||||||
|
return api.search(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
|
return api.getLibManga(track)
|
||||||
|
.map { remoteTrack ->
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String): Completable {
|
||||||
|
return api.login(username, password)
|
||||||
|
.doOnNext { interceptor.newAuth(it) }
|
||||||
|
.flatMap { api.getCurrentUser() }
|
||||||
|
.doOnNext { userId -> saveCredentials(username, userId) }
|
||||||
|
.doOnError { logout() }
|
||||||
|
.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
super.logout()
|
||||||
|
interceptor.newAuth(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUserId(): String {
|
||||||
|
return getPassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveToken(oauth: OAuth?) {
|
||||||
|
val json = gson.toJson(oauth)
|
||||||
|
preferences.trackToken(this).set(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreToken(): OAuth? {
|
||||||
|
return try {
|
||||||
|
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,200 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.*
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.*
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||||
|
|
||||||
|
private val rest = Retrofit.Builder()
|
||||||
|
.baseUrl(baseUrl)
|
||||||
|
.client(client.newBuilder().addInterceptor(interceptor).build())
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(KitsuApi.Rest::class.java)
|
||||||
|
|
||||||
|
fun addLibManga(track: Track, userId: String): Observable<Track> {
|
||||||
|
return Observable.defer {
|
||||||
|
// @formatter:off
|
||||||
|
val data = jsonObject(
|
||||||
|
"type" to "libraryEntries",
|
||||||
|
"attributes" to jsonObject(
|
||||||
|
"status" to track.toKitsuStatus(),
|
||||||
|
"progress" to track.last_chapter_read
|
||||||
|
),
|
||||||
|
"relationships" to jsonObject(
|
||||||
|
"user" to jsonObject(
|
||||||
|
"data" to jsonObject(
|
||||||
|
"id" to userId,
|
||||||
|
"type" to "users"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"media" to jsonObject(
|
||||||
|
"data" to jsonObject(
|
||||||
|
"id" to track.remote_id,
|
||||||
|
"type" to "manga"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
rest.addLibManga(jsonObject("data" to data))
|
||||||
|
.map { json ->
|
||||||
|
track.remote_id = json["data"]["id"].int
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLibManga(track: Track): Observable<Track> {
|
||||||
|
return Observable.defer {
|
||||||
|
// @formatter:off
|
||||||
|
val data = jsonObject(
|
||||||
|
"type" to "libraryEntries",
|
||||||
|
"id" to track.remote_id,
|
||||||
|
"attributes" to jsonObject(
|
||||||
|
"status" to track.toKitsuStatus(),
|
||||||
|
"progress" to track.last_chapter_read,
|
||||||
|
"rating" to track.toKitsuScore()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
rest.updateLibManga(track.remote_id, jsonObject("data" to data))
|
||||||
|
.map { track }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String): Observable<List<Track>> {
|
||||||
|
return rest.search(query)
|
||||||
|
.map { json ->
|
||||||
|
val data = json["data"].array
|
||||||
|
data.map { KitsuManga(it.obj) }
|
||||||
|
.filter { it.type != "novel" }
|
||||||
|
.map { it.toTrack() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findLibManga(track: Track, userId: String): Observable<Track?> {
|
||||||
|
return rest.findLibManga(track.remote_id, userId)
|
||||||
|
.map { json ->
|
||||||
|
val data = json["data"].array
|
||||||
|
if (data.size() > 0) {
|
||||||
|
KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibManga(track: Track): Observable<Track> {
|
||||||
|
return rest.getLibManga(track.remote_id)
|
||||||
|
.map { json ->
|
||||||
|
val data = json["data"].array
|
||||||
|
if (data.size() > 0) {
|
||||||
|
val include = json["included"].array[0].obj
|
||||||
|
KitsuLibManga(data[0].obj, include).toTrack()
|
||||||
|
} else {
|
||||||
|
throw Exception("Could not find manga")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun login(username: String, password: String): Observable<OAuth> {
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.baseUrl(loginUrl)
|
||||||
|
.client(client)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(KitsuApi.LoginRest::class.java)
|
||||||
|
.requestAccessToken(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentUser(): Observable<String> {
|
||||||
|
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface Rest {
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/vnd.api+json")
|
||||||
|
@POST("library-entries")
|
||||||
|
fun addLibManga(
|
||||||
|
@Body data: JsonObject
|
||||||
|
): Observable<JsonObject>
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/vnd.api+json")
|
||||||
|
@PATCH("library-entries/{id}")
|
||||||
|
fun updateLibManga(
|
||||||
|
@Path("id") remoteId: Int,
|
||||||
|
@Body data: JsonObject
|
||||||
|
): Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("manga")
|
||||||
|
fun search(
|
||||||
|
@Query("filter[text]", encoded = true) query: String
|
||||||
|
): Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("library-entries")
|
||||||
|
fun findLibManga(
|
||||||
|
@Query("filter[media_id]", encoded = true) remoteId: Int,
|
||||||
|
@Query("filter[user_id]", encoded = true) userId: String,
|
||||||
|
@Query("page[limit]", encoded = true) limit: Int = 10000,
|
||||||
|
@Query("include") includes: String = "media"
|
||||||
|
): Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("library-entries")
|
||||||
|
fun getLibManga(
|
||||||
|
@Query("filter[id]", encoded = true) remoteId: Int,
|
||||||
|
@Query("include") includes: String = "media"
|
||||||
|
): Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("users")
|
||||||
|
fun getCurrentUser(
|
||||||
|
@Query("filter[self]", encoded = true) self: Boolean = true
|
||||||
|
): Observable<JsonObject>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface LoginRest {
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("oauth/token")
|
||||||
|
fun requestAccessToken(
|
||||||
|
@Field("username") username: String,
|
||||||
|
@Field("password") password: String,
|
||||||
|
@Field("grant_type") grantType: String = "password",
|
||||||
|
@Field("client_id") client_id: String = clientId,
|
||||||
|
@Field("client_secret") client_secret: String = clientSecret
|
||||||
|
): Observable<OAuth>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||||
|
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||||
|
private const val baseUrl = "https://kitsu.io/api/edge/"
|
||||||
|
private const val loginUrl = "https://kitsu.io/api/"
|
||||||
|
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token",
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.build())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth object used for authenticated requests.
|
||||||
|
*/
|
||||||
|
private var oauth: OAuth? = kitsu.restoreToken()
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu")
|
||||||
|
|
||||||
|
val refreshToken = currAuth.refresh_token!!
|
||||||
|
|
||||||
|
// Refresh access token if expired.
|
||||||
|
if (currAuth.isExpired()) {
|
||||||
|
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
newAuth(gson.fromJson(response.body().string(), OAuth::class.java))
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the authorization header to the original request.
|
||||||
|
val authRequest = originalRequest.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
|
.header("Accept", "application/vnd.api+json")
|
||||||
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newAuth(oauth: OAuth?) {
|
||||||
|
this.oauth = oauth
|
||||||
|
kitsu.saveToken(oauth)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import android.support.annotation.CallSuper
|
||||||
|
import com.github.salomonbrys.kotson.*
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
|
||||||
|
open class KitsuManga(obj: JsonObject) {
|
||||||
|
val id by obj.byInt
|
||||||
|
val canonicalTitle by obj["attributes"].byString
|
||||||
|
val chapterCount = obj["attributes"].obj.get("chapterCount").nullInt
|
||||||
|
val type = obj["attributes"].obj.get("mangaType").nullString
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
open fun toTrack() = Track.create(TrackManager.KITSU).apply {
|
||||||
|
remote_id = this@KitsuManga.id
|
||||||
|
title = canonicalTitle
|
||||||
|
total_chapters = chapterCount ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
|
||||||
|
val remoteId by obj.byInt("id")
|
||||||
|
val status by obj["attributes"].byString
|
||||||
|
val rating = obj["attributes"].obj.get("rating").nullString
|
||||||
|
val progress by obj["attributes"].byInt
|
||||||
|
|
||||||
|
override fun toTrack() = super.toTrack().apply {
|
||||||
|
remote_id = remoteId
|
||||||
|
status = toTrackStatus()
|
||||||
|
score = rating?.let { it.toFloat() * 2 } ?: 0f
|
||||||
|
last_chapter_read = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toTrackStatus() = when (status) {
|
||||||
|
"current" -> Kitsu.READING
|
||||||
|
"completed" -> Kitsu.COMPLETED
|
||||||
|
"on_hold" -> Kitsu.ON_HOLD
|
||||||
|
"dropped" -> Kitsu.DROPPED
|
||||||
|
"planned" -> Kitsu.PLAN_TO_READ
|
||||||
|
else -> throw Exception("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Track.toKitsuStatus() = when (status) {
|
||||||
|
Kitsu.READING -> "current"
|
||||||
|
Kitsu.COMPLETED -> "completed"
|
||||||
|
Kitsu.ON_HOLD -> "on_hold"
|
||||||
|
Kitsu.DROPPED -> "dropped"
|
||||||
|
Kitsu.PLAN_TO_READ -> "planned"
|
||||||
|
else -> throw Exception("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Track.toKitsuScore(): String {
|
||||||
|
return if (score > 0) (score / 2).toString() else ""
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val created_at: Long,
|
||||||
|
val expires_in: Long,
|
||||||
|
val refresh_token: String?) {
|
||||||
|
|
||||||
|
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLAN_TO_READ = 6
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private val api by lazy { MyanimelistApi(client, getUsername(), getPassword()) }
|
||||||
|
|
||||||
|
override val name: String
|
||||||
|
get() = "MyAnimeList"
|
||||||
|
|
||||||
|
override fun getLogo() = R.drawable.mal
|
||||||
|
|
||||||
|
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatusList(): List<Int> {
|
||||||
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getScoreList(): List<String> {
|
||||||
|
return IntRange(0, 10).map(Int::toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun displayScore(track: Track): String {
|
||||||
|
return track.score.toInt().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(track: Track): Observable<Track> {
|
||||||
|
return api.addLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(track: Track): Observable<Track> {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.updateLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
|
return api.findLibManga(track, getUsername())
|
||||||
|
.flatMap { remoteTrack ->
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
update(track)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun search(query: String): Observable<List<Track>> {
|
||||||
|
return api.search(query, getUsername())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
|
return api.getLibManga(track, getUsername())
|
||||||
|
.map { remoteTrack ->
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String): Completable {
|
||||||
|
return api.login(username, password)
|
||||||
|
.doOnNext { saveCredentials(username, password) }
|
||||||
|
.doOnError { logout() }
|
||||||
|
.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Xml
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
|
import eu.kanade.tachiyomi.data.network.asObservable
|
||||||
|
import eu.kanade.tachiyomi.data.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
|
import okhttp3.*
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.xmlpull.v1.XmlSerializer
|
||||||
|
import rx.Observable
|
||||||
|
import java.io.StringWriter
|
||||||
|
|
||||||
|
class MyanimelistApi(private val client: OkHttpClient, username: String, password: String) {
|
||||||
|
|
||||||
|
private var headers = createHeaders(username, password)
|
||||||
|
|
||||||
|
fun addLibManga(track: Track): Observable<Track> {
|
||||||
|
return Observable.defer {
|
||||||
|
client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { track }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLibManga(track: Track): Observable<Track> {
|
||||||
|
return Observable.defer {
|
||||||
|
client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { track }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String, username: String): Observable<List<Track>> {
|
||||||
|
return if (query.startsWith(PREFIX_MY)) {
|
||||||
|
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
|
||||||
|
getList(username)
|
||||||
|
.flatMap { Observable.from(it) }
|
||||||
|
.filter { realQuery in it.title.toLowerCase() }
|
||||||
|
.toList()
|
||||||
|
} else {
|
||||||
|
client.newCall(GET(getSearchUrl(query), headers))
|
||||||
|
.asObservable()
|
||||||
|
.map { Jsoup.parse(it.body().string()) }
|
||||||
|
.flatMap { Observable.from(it.select("entry")) }
|
||||||
|
.filter { it.select("type").text() != "Novel" }
|
||||||
|
.map {
|
||||||
|
Track.create(TrackManager.MYANIMELIST).apply {
|
||||||
|
title = it.selectText("title")!!
|
||||||
|
remote_id = it.selectInt("id")
|
||||||
|
total_chapters = it.selectInt("chapters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getList(username: String): Observable<List<Track>> {
|
||||||
|
return client
|
||||||
|
.newCall(GET(getListUrl(username), headers))
|
||||||
|
.asObservable()
|
||||||
|
.map { Jsoup.parse(it.body().string()) }
|
||||||
|
.flatMap { Observable.from(it.select("manga")) }
|
||||||
|
.map {
|
||||||
|
Track.create(TrackManager.MYANIMELIST).apply {
|
||||||
|
title = it.selectText("series_title")!!
|
||||||
|
remote_id = it.selectInt("series_mangadb_id")
|
||||||
|
last_chapter_read = it.selectInt("my_read_chapters")
|
||||||
|
status = it.selectInt("my_status")
|
||||||
|
score = it.selectInt("my_score").toFloat()
|
||||||
|
total_chapters = it.selectInt("series_chapters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findLibManga(track: Track, username: String): Observable<Track?> {
|
||||||
|
return getList(username)
|
||||||
|
.map { list -> list.find { it.remote_id == track.remote_id } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibManga(track: Track, username: String): Observable<Track> {
|
||||||
|
return findLibManga(track, username)
|
||||||
|
.map { it ?: throw Exception("Could not find manga") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun login(username: String, password: String): Observable<Response> {
|
||||||
|
headers = createHeaders(username, password)
|
||||||
|
return client.newCall(GET(getLoginUrl(), headers))
|
||||||
|
.asObservable()
|
||||||
|
.doOnNext { response ->
|
||||||
|
response.close()
|
||||||
|
if (response.code() != 200) throw Exception("Login error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaPostPayload(track: Track): RequestBody {
|
||||||
|
val data = xml {
|
||||||
|
element(ENTRY_TAG) {
|
||||||
|
if (track.last_chapter_read != 0) {
|
||||||
|
text(CHAPTER_TAG, track.last_chapter_read.toString())
|
||||||
|
}
|
||||||
|
text(STATUS_TAG, track.status.toString())
|
||||||
|
text(SCORE_TAG, track.score.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormBody.Builder()
|
||||||
|
.add("data", data)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun xml(block: XmlSerializer.() -> Unit): String {
|
||||||
|
val x = Xml.newSerializer()
|
||||||
|
val writer = StringWriter()
|
||||||
|
|
||||||
|
with(x) {
|
||||||
|
setOutput(writer)
|
||||||
|
startDocument("UTF-8", false)
|
||||||
|
block()
|
||||||
|
endDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun XmlSerializer.element(tag: String, block: XmlSerializer.() -> Unit) {
|
||||||
|
startTag("", tag)
|
||||||
|
block()
|
||||||
|
endTag("", tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun XmlSerializer.text(tag: String, body: String) {
|
||||||
|
startTag("", tag)
|
||||||
|
text(body)
|
||||||
|
endTag("", tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLoginUrl() = Uri.parse(baseUrl).buildUpon()
|
||||||
|
.appendEncodedPath("api/account/verify_credentials.xml")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
fun getSearchUrl(query: String) = Uri.parse(baseUrl).buildUpon()
|
||||||
|
.appendEncodedPath("api/manga/search.xml")
|
||||||
|
.appendQueryParameter("q", query)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
fun getListUrl(username: String) = Uri.parse(baseUrl).buildUpon()
|
||||||
|
.appendPath("malappinfo.php")
|
||||||
|
.appendQueryParameter("u", username)
|
||||||
|
.appendQueryParameter("status", "all")
|
||||||
|
.appendQueryParameter("type", "manga")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
||||||
|
.appendEncodedPath("api/mangalist/update")
|
||||||
|
.appendPath("${track.remote_id}.xml")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
||||||
|
.appendEncodedPath("api/mangalist/add")
|
||||||
|
.appendPath("${track.remote_id}.xml")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
fun createHeaders(username: String, password: String): Headers {
|
||||||
|
return Headers.Builder()
|
||||||
|
.add("Authorization", Credentials.basic(username, password))
|
||||||
|
.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val baseUrl = "https://myanimelist.net"
|
||||||
|
|
||||||
|
private val ENTRY_TAG = "entry"
|
||||||
|
private val CHAPTER_TAG = "chapter"
|
||||||
|
private val SCORE_TAG = "score"
|
||||||
|
private val STATUS_TAG = "status"
|
||||||
|
|
||||||
|
const val PREFIX_MY = "my:"
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.updater
|
|||||||
import android.app.IntentService
|
import android.app.IntentService
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -36,21 +35,6 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt user with apk install intent
|
|
||||||
* @param context context
|
|
||||||
* @param file file of apk that is installed
|
|
||||||
*/
|
|
||||||
fun installAPK(context: Context, file: File) {
|
|
||||||
// Prompt install interface
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
|
|
||||||
// Without this flag android returned a intent error!
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,7 +90,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
throw Exception("Unsuccessful response")
|
throw Exception("Unsuccessful response")
|
||||||
}
|
}
|
||||||
|
|
||||||
val installIntent = UpdateNotificationReceiver.installApkIntent(ctx, apkFile.absolutePath)
|
val installIntent = UpdateNotificationReceiver.installApkIntent(ctx, apkFile)
|
||||||
|
|
||||||
// Prompt the user to install the new update.
|
// Prompt the user to install the new update.
|
||||||
NotificationCompat.Builder(this).update {
|
NotificationCompat.Builder(this).update {
|
||||||
|
@ -4,6 +4,10 @@ import android.app.PendingIntent
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.support.v4.content.FileProvider
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -12,34 +16,14 @@ class UpdateNotificationReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
ACTION_INSTALL_APK -> {
|
|
||||||
UpdateDownloaderService.installAPK(context,
|
|
||||||
File(intent.getStringExtra(EXTRA_FILE_LOCATION)))
|
|
||||||
cancelNotification(context)
|
|
||||||
}
|
|
||||||
ACTION_DOWNLOAD_UPDATE -> UpdateDownloaderService.downloadUpdate(context,
|
|
||||||
intent.getStringExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL))
|
|
||||||
ACTION_CANCEL_NOTIFICATION -> cancelNotification(context)
|
ACTION_CANCEL_NOTIFICATION -> cancelNotification(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelNotification(context: Context) {
|
|
||||||
context.notificationManager.cancel(NOTIFICATION_UPDATER_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Install apk action
|
|
||||||
const val ACTION_INSTALL_APK = "eu.kanade.INSTALL_APK"
|
|
||||||
|
|
||||||
// Download apk action
|
|
||||||
const val ACTION_DOWNLOAD_UPDATE = "eu.kanade.RETRY_DOWNLOAD"
|
|
||||||
|
|
||||||
// Cancel notification action
|
// Cancel notification action
|
||||||
const val ACTION_CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
|
const val ACTION_CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
|
||||||
|
|
||||||
// Absolute path of apk file
|
|
||||||
const val EXTRA_FILE_LOCATION = "file_location"
|
|
||||||
|
|
||||||
fun cancelNotificationIntent(context: Context): PendingIntent {
|
fun cancelNotificationIntent(context: Context): PendingIntent {
|
||||||
val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
|
val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
|
||||||
action = ACTION_CANCEL_NOTIFICATION
|
action = ACTION_CANCEL_NOTIFICATION
|
||||||
@ -47,20 +31,39 @@ class UpdateNotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, 0, intent, 0)
|
return PendingIntent.getBroadcast(context, 0, intent, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installApkIntent(context: Context, path: String): PendingIntent {
|
/**
|
||||||
val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
|
* Prompt user with apk install intent
|
||||||
action = ACTION_INSTALL_APK
|
*
|
||||||
putExtra(EXTRA_FILE_LOCATION, path)
|
* @param context context
|
||||||
|
* @param file file of apk that is installed
|
||||||
|
*/
|
||||||
|
fun installApkIntent(context: Context, file: File): PendingIntent {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||||
|
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
||||||
|
else Uri.fromFile(file)
|
||||||
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, 0)
|
cancelNotification(context)
|
||||||
|
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a new update and let the user install the new version from a notification.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @param url the url to the new update.
|
||||||
|
*/
|
||||||
fun downloadApkIntent(context: Context, url: String): PendingIntent {
|
fun downloadApkIntent(context: Context, url: String): PendingIntent {
|
||||||
val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
|
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
||||||
action = ACTION_DOWNLOAD_UPDATE
|
|
||||||
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
|
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, 0)
|
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelNotification(context: Context) {
|
||||||
|
context.notificationManager.cancel(NOTIFICATION_UPDATER_ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,11 +15,17 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
interface ActivityMixin {
|
interface ActivityMixin {
|
||||||
|
|
||||||
|
var resumed: Boolean
|
||||||
|
|
||||||
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
|
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
|
||||||
if (backNavigation) {
|
if (backNavigation) {
|
||||||
toolbar.setNavigationOnClickListener { onBackPressed() }
|
toolbar.setNavigationOnClickListener {
|
||||||
|
if (resumed) {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,26 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
package eu.kanade.tachiyomi.ui.base.activity
|
||||||
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
||||||
|
|
||||||
|
override var resumed = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
LocaleHelper.updateConfiguration(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getActivity() = this
|
override fun getActivity() = this
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
resumed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
resumed = false
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,17 @@ package eu.kanade.tachiyomi.ui.base.activity
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
import nucleus.view.NucleusAppCompatActivity
|
import nucleus.view.NucleusAppCompatActivity
|
||||||
|
|
||||||
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
||||||
|
|
||||||
|
override var resumed = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
LocaleHelper.updateConfiguration(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
val superFactory = presenterFactory
|
val superFactory = presenterFactory
|
||||||
setPresenterFactory {
|
setPresenterFactory {
|
||||||
@ -20,4 +27,14 @@ abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P
|
|||||||
|
|
||||||
override fun getActivity() = this
|
override fun getActivity() = this
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
resumed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
resumed = false
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -221,6 +221,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
|||||||
|
|
||||||
// Setup filters button
|
// Setup filters button
|
||||||
menu.findItem(R.id.action_set_filter).apply {
|
menu.findItem(R.id.action_set_filter).apply {
|
||||||
|
icon.mutate()
|
||||||
if (presenter.source.filters.isEmpty()) {
|
if (presenter.source.filters.isEmpty()) {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
icon.alpha = 128
|
icon.alpha = 128
|
||||||
|
@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
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.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
@ -21,7 +20,7 @@ import rx.schedulers.Schedulers
|
|||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.NoSuchElementException
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [CatalogueFragment].
|
* Presenter of [CatalogueFragment].
|
||||||
@ -333,13 +332,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
|
|
||||||
// Ensure at least one language
|
// Ensure at least one language
|
||||||
if (languages.isEmpty()) {
|
if (languages.isEmpty()) {
|
||||||
languages.add(EN.code)
|
languages.add("en")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sourceManager.getOnlineSources()
|
return sourceManager.getOnlineSources()
|
||||||
.filter { it.lang.code in languages }
|
.filter { it.lang in languages }
|
||||||
.filterNot { it.id.toString() in hiddenCatalogues }
|
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||||
.sortedBy { "(${it.lang.code}) ${it.name}" }
|
.sortedBy { "(${it.lang}) ${it.name}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,7 +27,9 @@ import nucleus.factory.RequiresPresenter
|
|||||||
* UI related actions should be called from here.
|
* UI related actions should be called from here.
|
||||||
*/
|
*/
|
||||||
@RequiresPresenter(CategoryPresenter::class)
|
@RequiresPresenter(CategoryPresenter::class)
|
||||||
class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener, OnStartDragListener {
|
class CategoryActivity :
|
||||||
|
BaseRxActivity<CategoryPresenter>(),
|
||||||
|
ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener, OnStartDragListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object used to show actionMode toolbar.
|
* Object used to show actionMode toolbar.
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.category
|
package eu.kanade.tachiyomi.ui.category
|
||||||
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
@ -17,18 +16,10 @@ import java.util.*
|
|||||||
* @param activity activity that created adapter
|
* @param activity activity that created adapter
|
||||||
* @constructor Creates a CategoryAdapter object
|
* @constructor Creates a CategoryAdapter object
|
||||||
*/
|
*/
|
||||||
class CategoryAdapter(private val activity: CategoryActivity) : FlexibleAdapter<CategoryHolder, Category>(), ItemTouchHelperAdapter {
|
class CategoryAdapter(private val activity: CategoryActivity) :
|
||||||
|
FlexibleAdapter<CategoryHolder, Category>(), ItemTouchHelperAdapter {
|
||||||
/**
|
|
||||||
* Generator used to generate circle letter icons
|
|
||||||
*/
|
|
||||||
private val generator: ColorGenerator
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Let generator use Material Design colors.
|
|
||||||
// Material design is love, material design is live!
|
|
||||||
generator = ColorGenerator.MATERIAL
|
|
||||||
|
|
||||||
// Set unique id's
|
// Set unique id's
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
}
|
}
|
||||||
@ -54,7 +45,7 @@ class CategoryAdapter(private val activity: CategoryActivity) : FlexibleAdapter<
|
|||||||
override fun onBindViewHolder(holder: CategoryHolder, position: Int) {
|
override fun onBindViewHolder(holder: CategoryHolder, position: Int) {
|
||||||
// Update holder values.
|
// Update holder values.
|
||||||
val category = getItem(position)
|
val category = getItem(position)
|
||||||
holder.onSetValues(category, generator)
|
holder.onSetValues(category)
|
||||||
|
|
||||||
//When user scrolls this bind the correct selection status
|
//When user scrolls this bind the correct selection status
|
||||||
holder.itemView.isActivated = isSelected(position)
|
holder.itemView.isActivated = isSelected(position)
|
||||||
|
@ -24,7 +24,12 @@ import kotlinx.android.synthetic.main.item_edit_categories.view.*
|
|||||||
*
|
*
|
||||||
* @constructor Create CategoryHolder object
|
* @constructor Create CategoryHolder object
|
||||||
*/
|
*/
|
||||||
class CategoryHolder(view: View, adapter: CategoryAdapter, listener: FlexibleViewHolder.OnListItemClickListener, dragListener: OnStartDragListener) : FlexibleViewHolder(view, adapter, listener) {
|
class CategoryHolder(
|
||||||
|
view: View,
|
||||||
|
adapter: CategoryAdapter,
|
||||||
|
listener: FlexibleViewHolder.OnListItemClickListener,
|
||||||
|
dragListener: OnStartDragListener
|
||||||
|
) : FlexibleViewHolder(view, adapter, listener) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Create round letter image onclick to simulate long click
|
// Create round letter image onclick to simulate long click
|
||||||
@ -46,29 +51,31 @@ class CategoryHolder(view: View, adapter: CategoryAdapter, listener: FlexibleVie
|
|||||||
* Update category item values.
|
* Update category item values.
|
||||||
*
|
*
|
||||||
* @param category category of item.
|
* @param category category of item.
|
||||||
* @param generator generator used to generate circle letter icons.
|
|
||||||
*/
|
*/
|
||||||
fun onSetValues(category: Category, generator: ColorGenerator) {
|
fun onSetValues(category: Category) {
|
||||||
// Set capitalized title.
|
// Set capitalized title.
|
||||||
itemView.title.text = category.name.capitalize()
|
itemView.title.text = category.name.capitalize()
|
||||||
|
|
||||||
// Update circle letter image.
|
// Update circle letter image.
|
||||||
itemView.image.setImageDrawable(getRound(category.name.substring(0, 1).toUpperCase(), generator))
|
itemView.post {
|
||||||
|
itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns circle letter image
|
* Returns circle letter image
|
||||||
*
|
*
|
||||||
* @param text first letter of string
|
* @param text first letter of string
|
||||||
* @param generator the generator used to generate circle letter image
|
|
||||||
*/
|
*/
|
||||||
private fun getRound(text: String, generator: ColorGenerator): TextDrawable {
|
private fun getRound(text: String): TextDrawable {
|
||||||
|
val size = Math.min(itemView.image.width, itemView.image.height)
|
||||||
return TextDrawable.builder()
|
return TextDrawable.builder()
|
||||||
.beginConfig()
|
.beginConfig()
|
||||||
|
.width(size)
|
||||||
|
.height(size)
|
||||||
.textColor(Color.WHITE)
|
.textColor(Color.WHITE)
|
||||||
.useFont(Typeface.DEFAULT)
|
.useFont(Typeface.DEFAULT)
|
||||||
.toUpperCase()
|
|
||||||
.endConfig()
|
.endConfig()
|
||||||
.buildRound(text, generator.getColor(text))
|
.buildRound(text, ColorGenerator.MATERIAL.getColor(text))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -48,8 +48,9 @@ class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
|||||||
* Updates the progress bar of the download.
|
* Updates the progress bar of the download.
|
||||||
*/
|
*/
|
||||||
fun notifyProgress() {
|
fun notifyProgress() {
|
||||||
|
val pages = download.pages ?: return
|
||||||
if (view.download_progress.max == 1) {
|
if (view.download_progress.max == 1) {
|
||||||
view.download_progress.max = download.pages!!.size * 100
|
view.download_progress.max = pages.size * 100
|
||||||
}
|
}
|
||||||
view.download_progress.progress = download.totalProgress
|
view.download_progress.progress = download.totalProgress
|
||||||
}
|
}
|
||||||
@ -58,7 +59,8 @@ class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
|||||||
* Updates the text field of the number of downloaded pages.
|
* Updates the text field of the number of downloaded pages.
|
||||||
*/
|
*/
|
||||||
fun notifyDownloadedPages() {
|
fun notifyDownloadedPages() {
|
||||||
view.download_progress_text.text = "${download.downloadedImages}/${download.pages!!.size}"
|
val pages = download.pages ?: return
|
||||||
|
view.download_progress_text.text = "${download.downloadedImages}/${pages.size}"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
|||||||
/**
|
/**
|
||||||
* The list of manga in this category.
|
* The list of manga in this category.
|
||||||
*/
|
*/
|
||||||
private var mangas: List<Manga>? = null
|
private var mangas: List<Manga> = emptyList()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
@ -37,7 +37,7 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
|||||||
fun setItems(list: List<Manga>) {
|
fun setItems(list: List<Manga>) {
|
||||||
mItems = list
|
mItems = list
|
||||||
|
|
||||||
// A copy of manga that it's always unfiltered
|
// A copy of manga always unfiltered.
|
||||||
mangas = ArrayList(list)
|
mangas = ArrayList(list)
|
||||||
updateDataSet(null)
|
updateDataSet(null)
|
||||||
}
|
}
|
||||||
@ -58,10 +58,8 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
|||||||
* @param param the filter. Not used.
|
* @param param the filter. Not used.
|
||||||
*/
|
*/
|
||||||
override fun updateDataSet(param: String?) {
|
override fun updateDataSet(param: String?) {
|
||||||
mangas?.let {
|
filterItems(mangas)
|
||||||
filterItems(it)
|
notifyDataSetChanged()
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.library
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.TabLayout
|
import android.support.design.widget.TabLayout
|
||||||
|
import android.support.v4.graphics.drawable.DrawableCompat
|
||||||
import android.support.v4.view.ViewPager
|
import android.support.v4.view.ViewPager
|
||||||
|
import android.support.v4.widget.DrawerLayout
|
||||||
import android.support.v7.view.ActionMode
|
import android.support.v7.view.ActionMode
|
||||||
import android.support.v7.widget.SearchView
|
import android.support.v7.widget.SearchView
|
||||||
import android.view.*
|
import android.view.*
|
||||||
@ -20,6 +23,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryActivity
|
import eu.kanade.tachiyomi.ui.category.CategoryActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import kotlinx.android.synthetic.main.fragment_library.*
|
import kotlinx.android.synthetic.main.fragment_library.*
|
||||||
@ -73,22 +77,36 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
*/
|
*/
|
||||||
private var selectedCoverManga: Manga? = null
|
private var selectedCoverManga: Manga? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Status of isFilterDownloaded
|
|
||||||
*/
|
|
||||||
var isFilterDownloaded = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status of isFilterUnread
|
|
||||||
*/
|
|
||||||
var isFilterUnread = false
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of manga per row in grid mode.
|
* Number of manga per row in grid mode.
|
||||||
*/
|
*/
|
||||||
var mangaPerRow = 0
|
var mangaPerRow = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation view containing filter/sort/display items.
|
||||||
|
*/
|
||||||
|
private lateinit var navView: LibraryNavigationView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawer listener to allow swipe only for closing the drawer.
|
||||||
|
*/
|
||||||
|
private val drawerListener by lazy {
|
||||||
|
object : DrawerLayout.SimpleDrawerListener() {
|
||||||
|
override fun onDrawerClosed(drawerView: View) {
|
||||||
|
if (drawerView == navView) {
|
||||||
|
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawerOpened(drawerView: View) {
|
||||||
|
if (drawerView == navView) {
|
||||||
|
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, navView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription for the number of manga per row.
|
* Subscription for the number of manga per row.
|
||||||
*/
|
*/
|
||||||
@ -123,8 +141,6 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
isFilterDownloaded = preferences.filterDownloaded().get() as Boolean
|
|
||||||
isFilterUnread = preferences.filterUnread().get() as Boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
||||||
@ -146,7 +162,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
if (savedState != null) {
|
if (savedState != null) {
|
||||||
activeCategory = savedState.getInt(CATEGORY_KEY)
|
activeCategory = savedState.getInt(CATEGORY_KEY)
|
||||||
query = savedState.getString(QUERY_KEY)
|
query = savedState.getString(QUERY_KEY)
|
||||||
presenter.searchSubject.onNext(query)
|
presenter.searchSubject.call(query)
|
||||||
if (presenter.selectedMangas.isNotEmpty()) {
|
if (presenter.selectedMangas.isNotEmpty()) {
|
||||||
createActionModeIfNeeded()
|
createActionModeIfNeeded()
|
||||||
}
|
}
|
||||||
@ -159,6 +175,25 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
.skip(1)
|
.skip(1)
|
||||||
// Set again the adapter to recalculate the covers height
|
// Set again the adapter to recalculate the covers height
|
||||||
.subscribe { reattachAdapter() }
|
.subscribe { reattachAdapter() }
|
||||||
|
|
||||||
|
|
||||||
|
// Inflate and prepare drawer
|
||||||
|
navView = activity.drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
|
||||||
|
activity.drawer.addView(navView)
|
||||||
|
activity.drawer.addDrawerListener(drawerListener)
|
||||||
|
|
||||||
|
navView.post {
|
||||||
|
if (isAdded && !activity.drawer.isDrawerOpen(navView))
|
||||||
|
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
|
||||||
|
}
|
||||||
|
|
||||||
|
navView.onGroupClicked = { group ->
|
||||||
|
when (group) {
|
||||||
|
is LibraryNavigationView.FilterGroup -> onFilterChanged()
|
||||||
|
is LibraryNavigationView.SortGroup -> onSortChanged()
|
||||||
|
is LibraryNavigationView.DisplayGroup -> reattachAdapter()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@ -167,6 +202,8 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
activity.drawer.removeDrawerListener(drawerListener)
|
||||||
|
activity.drawer.removeView(navView)
|
||||||
numColumnsSubscription?.unsubscribe()
|
numColumnsSubscription?.unsubscribe()
|
||||||
tabs.setupWithViewPager(null)
|
tabs.setupWithViewPager(null)
|
||||||
tabs.visibility = View.GONE
|
tabs.visibility = View.GONE
|
||||||
@ -182,9 +219,6 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.library, menu)
|
inflater.inflate(R.menu.library, menu)
|
||||||
|
|
||||||
// Initialize search menu
|
|
||||||
val filterDownloadedItem = menu.findItem(R.id.action_filter_downloaded)
|
|
||||||
val filterUnreadItem = menu.findItem(R.id.action_filter_unread)
|
|
||||||
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
|
||||||
|
|
||||||
@ -194,8 +228,8 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
searchView.clearFocus()
|
searchView.clearFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
filterDownloadedItem.isChecked = isFilterDownloaded
|
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
||||||
filterUnreadItem.isChecked = isFilterUnread
|
menu.findItem(R.id.action_filter).icon.mutate()
|
||||||
|
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
@ -211,35 +245,19 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
val filterItem = menu.findItem(R.id.action_filter)
|
||||||
|
|
||||||
|
// Tint icon if there's a filter active
|
||||||
|
val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE
|
||||||
|
DrawableCompat.setTint(filterItem.icon, filterColor)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_filter_unread -> {
|
R.id.action_filter -> {
|
||||||
// Change unread filter status.
|
activity.drawer.openDrawer(Gravity.END)
|
||||||
isFilterUnread = !isFilterUnread
|
|
||||||
// Update settings.
|
|
||||||
preferences.filterUnread().set(isFilterUnread)
|
|
||||||
// Apply filter.
|
|
||||||
onFilterCheckboxChanged()
|
|
||||||
}
|
}
|
||||||
R.id.action_filter_downloaded -> {
|
|
||||||
// Change downloaded filter status.
|
|
||||||
isFilterDownloaded = !isFilterDownloaded
|
|
||||||
// Update settings.
|
|
||||||
preferences.filterDownloaded().set(isFilterDownloaded)
|
|
||||||
// Apply filter.
|
|
||||||
onFilterCheckboxChanged()
|
|
||||||
}
|
|
||||||
R.id.action_filter_empty -> {
|
|
||||||
// Remove filter status.
|
|
||||||
isFilterUnread = false
|
|
||||||
isFilterDownloaded = false
|
|
||||||
// Update settings.
|
|
||||||
preferences.filterUnread().set(isFilterUnread)
|
|
||||||
preferences.filterDownloaded().set(isFilterDownloaded)
|
|
||||||
// Apply filter
|
|
||||||
onFilterCheckboxChanged()
|
|
||||||
}
|
|
||||||
R.id.action_library_display_mode -> swapDisplayMode()
|
|
||||||
R.id.action_update_library -> {
|
R.id.action_update_library -> {
|
||||||
LibraryUpdateService.start(activity)
|
LibraryUpdateService.start(activity)
|
||||||
}
|
}
|
||||||
@ -254,19 +272,18 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies filter change
|
* Called when a filter is changed.
|
||||||
*/
|
*/
|
||||||
private fun onFilterCheckboxChanged() {
|
private fun onFilterChanged() {
|
||||||
presenter.resubscribeLibrary()
|
presenter.requestFilterUpdate()
|
||||||
activity.supportInvalidateOptionsMenu()
|
activity.supportInvalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swap display mode
|
* Called when the sorting mode is changed.
|
||||||
*/
|
*/
|
||||||
private fun swapDisplayMode() {
|
private fun onSortChanged() {
|
||||||
presenter.swapDisplayMode()
|
presenter.requestSortUpdate()
|
||||||
reattachAdapter()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -302,7 +319,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
|
|
||||||
// Notify the subject the query has changed.
|
// Notify the subject the query has changed.
|
||||||
if (isResumed) {
|
if (isResumed) {
|
||||||
presenter.searchSubject.onNext(query)
|
presenter.searchSubject.call(query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,7 +347,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
view_pager.post { if (isAdded) tabs.setScrollPosition(view_pager.currentItem, 0f, true) }
|
view_pager.post { if (isAdded) tabs.setScrollPosition(view_pager.currentItem, 0f, true) }
|
||||||
|
|
||||||
// Send the manga map to child fragments after the adapter is updated.
|
// Send the manga map to child fragments after the adapter is updated.
|
||||||
presenter.libraryMangaSubject.onNext(LibraryMangaEvent(mangaMap))
|
presenter.libraryMangaSubject.call(LibraryMangaEvent(mangaMap))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,191 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC
|
||||||
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC
|
||||||
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The navigation view shown in a drawer with the different options to show the library.
|
||||||
|
*/
|
||||||
|
class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
||||||
|
: ExtendedNavigationView(context, attrs) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences helper.
|
||||||
|
*/
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of groups shown in the view.
|
||||||
|
*/
|
||||||
|
private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter instance.
|
||||||
|
*/
|
||||||
|
private val adapter = Adapter(groups.map { it.createItems() }.flatten())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click listener to notify the parent fragment when an item from a group is clicked.
|
||||||
|
*/
|
||||||
|
var onGroupClicked: (Group) -> Unit = {}
|
||||||
|
|
||||||
|
init {
|
||||||
|
recycler.adapter = adapter
|
||||||
|
|
||||||
|
groups.forEach { it.initModels() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there's at least one filter from [FilterGroup] active.
|
||||||
|
*/
|
||||||
|
fun hasActiveFilters(): Boolean {
|
||||||
|
return (groups[0] as FilterGroup).items.any { it.checked }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter of the recycler view.
|
||||||
|
*/
|
||||||
|
inner class Adapter(items: List<Item>) : ExtendedNavigationView.Adapter(items) {
|
||||||
|
|
||||||
|
override fun onItemClicked(item: Item) {
|
||||||
|
if (item is GroupedItem) {
|
||||||
|
item.group.onItemClicked(item)
|
||||||
|
onGroupClicked(item.group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters group (unread, downloaded, ...).
|
||||||
|
*/
|
||||||
|
inner class FilterGroup : Group {
|
||||||
|
|
||||||
|
private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this)
|
||||||
|
|
||||||
|
private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
|
||||||
|
|
||||||
|
override val items = listOf(downloaded, unread)
|
||||||
|
|
||||||
|
override val header = Item.Header(R.string.action_filter)
|
||||||
|
|
||||||
|
override val footer = Item.Separator()
|
||||||
|
|
||||||
|
override fun initModels() {
|
||||||
|
downloaded.checked = preferences.filterDownloaded().getOrDefault()
|
||||||
|
unread.checked = preferences.filterUnread().getOrDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClicked(item: Item) {
|
||||||
|
item as Item.CheckboxGroup
|
||||||
|
item.checked = !item.checked
|
||||||
|
when (item) {
|
||||||
|
downloaded -> preferences.filterDownloaded().set(item.checked)
|
||||||
|
unread -> preferences.filterUnread().set(item.checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.notifyItemChanged(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorting group (alphabetically, by last read, ...) and ascending or descending.
|
||||||
|
*/
|
||||||
|
inner class SortGroup : Group {
|
||||||
|
|
||||||
|
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
|
||||||
|
|
||||||
|
private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
|
||||||
|
|
||||||
|
private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this)
|
||||||
|
|
||||||
|
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
|
||||||
|
|
||||||
|
override val items = listOf(alphabetically, lastRead, lastUpdated, unread)
|
||||||
|
|
||||||
|
override val header = Item.Header(R.string.action_sort)
|
||||||
|
|
||||||
|
override val footer = Item.Separator()
|
||||||
|
|
||||||
|
override fun initModels() {
|
||||||
|
val sorting = preferences.librarySortingMode().getOrDefault()
|
||||||
|
val order = if (preferences.librarySortingAscending().getOrDefault())
|
||||||
|
SORT_ASC else SORT_DESC
|
||||||
|
|
||||||
|
alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE
|
||||||
|
lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE
|
||||||
|
lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
|
||||||
|
unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClicked(item: Item) {
|
||||||
|
item as Item.MultiStateGroup
|
||||||
|
val prevState = item.state
|
||||||
|
|
||||||
|
item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE }
|
||||||
|
item.state = when (prevState) {
|
||||||
|
SORT_NONE -> SORT_ASC
|
||||||
|
SORT_ASC -> SORT_DESC
|
||||||
|
SORT_DESC -> SORT_ASC
|
||||||
|
else -> throw Exception("Unknown state")
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences.librarySortingMode().set(when (item) {
|
||||||
|
alphabetically -> LibrarySort.ALPHA
|
||||||
|
lastRead -> LibrarySort.LAST_READ
|
||||||
|
lastUpdated -> LibrarySort.LAST_UPDATED
|
||||||
|
unread -> LibrarySort.UNREAD
|
||||||
|
else -> throw Exception("Unknown sorting")
|
||||||
|
})
|
||||||
|
preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
|
||||||
|
|
||||||
|
item.group.items.forEach { adapter.notifyItemChanged(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display group, to show the library as a list or a grid.
|
||||||
|
*/
|
||||||
|
inner class DisplayGroup : Group {
|
||||||
|
|
||||||
|
private val grid = Item.Radio(R.string.action_display_grid, this)
|
||||||
|
|
||||||
|
private val list = Item.Radio(R.string.action_display_list, this)
|
||||||
|
|
||||||
|
override val items = listOf(grid, list)
|
||||||
|
|
||||||
|
override val header = Item.Header(R.string.action_display)
|
||||||
|
|
||||||
|
override val footer = null
|
||||||
|
|
||||||
|
override fun initModels() {
|
||||||
|
val asList = preferences.libraryAsList().getOrDefault()
|
||||||
|
grid.checked = !asList
|
||||||
|
list.checked = asList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClicked(item: Item) {
|
||||||
|
item as Item.Radio
|
||||||
|
if (item.checked) return
|
||||||
|
|
||||||
|
item.group.items.forEach { (it as Item.Radio).checked = false }
|
||||||
|
item.checked = true
|
||||||
|
|
||||||
|
preferences.libraryAsList().set(if (item == list) true else false)
|
||||||
|
|
||||||
|
item.group.items.forEach { adapter.notifyItemChanged(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.library
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Pair
|
import android.util.Pair
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
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
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
@ -12,11 +15,12 @@ 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.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.util.combineLatest
|
||||||
|
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subjects.BehaviorSubject
|
|
||||||
import rx.subjects.PublishSubject
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -27,6 +31,31 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database.
|
||||||
|
*/
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences.
|
||||||
|
*/
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cover cache.
|
||||||
|
*/
|
||||||
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source manager.
|
||||||
|
*/
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download manager.
|
||||||
|
*/
|
||||||
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Categories of the library.
|
* Categories of the library.
|
||||||
*/
|
*/
|
||||||
@ -40,61 +69,139 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
/**
|
/**
|
||||||
* Search query of the library.
|
* Search query of the library.
|
||||||
*/
|
*/
|
||||||
val searchSubject: BehaviorSubject<String> = BehaviorSubject.create()
|
val searchSubject: BehaviorRelay<String> = BehaviorRelay.create()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subject to notify the library's viewpager for updates.
|
* Subject to notify the library's viewpager for updates.
|
||||||
*/
|
*/
|
||||||
val libraryMangaSubject: BehaviorSubject<LibraryMangaEvent> = BehaviorSubject.create()
|
val libraryMangaSubject: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subject to notify the UI of selection updates.
|
* Subject to notify the UI of selection updates.
|
||||||
*/
|
*/
|
||||||
val selectionSubject: PublishSubject<LibrarySelectionEvent> = PublishSubject.create()
|
val selectionSubject: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database.
|
* Relay used to apply the UI filters to the last emission of the library.
|
||||||
*/
|
*/
|
||||||
val db: DatabaseHelper by injectLazy()
|
private val filterTriggerRelay = BehaviorRelay.create(Unit)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preferences.
|
* Relay used to apply the selected sorting method to the last emission of the library.
|
||||||
*/
|
*/
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
private val sortTriggerRelay = BehaviorRelay.create(Unit)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cover cache.
|
* Library subscription.
|
||||||
*/
|
*/
|
||||||
val coverCache: CoverCache by injectLazy()
|
private var librarySubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
val sourceManager: SourceManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download manager.
|
|
||||||
*/
|
|
||||||
val downloadManager: DownloadManager by injectLazy()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Id of the restartable that listens for library updates.
|
|
||||||
*/
|
|
||||||
const val GET_LIBRARY = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
subscribeLibrary()
|
||||||
|
}
|
||||||
|
|
||||||
restartableLatestCache(GET_LIBRARY,
|
/**
|
||||||
{ getLibraryObservable() },
|
* Subscribes to library if needed.
|
||||||
{ view, pair -> view.onNextLibraryUpdate(pair.first, pair.second) })
|
*/
|
||||||
|
fun subscribeLibrary() {
|
||||||
|
if (librarySubscription.isNullOrUnsubscribed()) {
|
||||||
|
librarySubscription = getLibraryObservable()
|
||||||
|
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
|
||||||
|
{ lib, tick -> Pair(lib.first, applyFilters(lib.second)) })
|
||||||
|
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
|
||||||
|
{ lib, tick -> Pair(lib.first, applySort(lib.second)) })
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeLatestCache({ view, pair ->
|
||||||
|
view.onNextLibraryUpdate(pair.first, pair.second)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (savedState == null) {
|
/**
|
||||||
start(GET_LIBRARY)
|
* Applies library filters to the given map of manga.
|
||||||
|
*
|
||||||
|
* @param map the map to filter.
|
||||||
|
*/
|
||||||
|
private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||||
|
// Cached list of downloaded manga directories given a source id.
|
||||||
|
val mangaDirectories = mutableMapOf<Int, Array<UniFile>>()
|
||||||
|
|
||||||
|
// Cached list of downloaded chapter directories for a manga.
|
||||||
|
val chapterDirectories = mutableMapOf<Long, Boolean>()
|
||||||
|
|
||||||
|
val filterDownloaded = preferences.filterDownloaded().getOrDefault()
|
||||||
|
|
||||||
|
val filterUnread = preferences.filterUnread().getOrDefault()
|
||||||
|
|
||||||
|
val filterFn: (Manga) -> Boolean = f@ { manga ->
|
||||||
|
// Filter out manga without source.
|
||||||
|
val source = sourceManager.get(manga.source) ?: return@f false
|
||||||
|
|
||||||
|
// Filter when there isn't unread chapters.
|
||||||
|
if (filterUnread && manga.unread == 0) {
|
||||||
|
return@f false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter when the download directory doesn't exist or is null.
|
||||||
|
if (filterDownloaded) {
|
||||||
|
val mangaDirs = mangaDirectories.getOrPut(source.id) {
|
||||||
|
downloadManager.findSourceDir(source)?.listFiles() ?: emptyArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaDirName = downloadManager.getMangaDirName(manga)
|
||||||
|
val mangaDir = mangaDirs.find { it.name == mangaDirName } ?: return@f false
|
||||||
|
|
||||||
|
val hasDirs = chapterDirectories.getOrPut(manga.id!!) {
|
||||||
|
(mangaDir.listFiles() ?: emptyArray()).isNotEmpty()
|
||||||
|
}
|
||||||
|
if (!hasDirs) {
|
||||||
|
return@f false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies library sorting to the given map of manga.
|
||||||
|
*
|
||||||
|
* @param map the map to sort.
|
||||||
|
*/
|
||||||
|
private fun applySort(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||||
|
val sortingMode = preferences.librarySortingMode().getOrDefault()
|
||||||
|
|
||||||
|
// TODO lazy initialization in kotlin 1.1
|
||||||
|
var lastReadManga: Map<Long, Int>? = null
|
||||||
|
if (sortingMode == LibrarySort.LAST_READ) {
|
||||||
|
var counter = 0
|
||||||
|
lastReadManga = db.getLastReadManga().executeAsBlocking()
|
||||||
|
.associate { it.id!! to counter++ }
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortFn: (Manga, Manga) -> Int = { manga1, manga2 ->
|
||||||
|
when (sortingMode) {
|
||||||
|
LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title)
|
||||||
|
LibrarySort.LAST_READ -> {
|
||||||
|
// Get index of manga, set equal to list if size unknown.
|
||||||
|
val manga1LastRead = lastReadManga!![manga1.id!!] ?: lastReadManga!!.size
|
||||||
|
val manga2LastRead = lastReadManga!![manga2.id!!] ?: lastReadManga!!.size
|
||||||
|
manga1LastRead.compareTo(manga2LastRead)
|
||||||
|
}
|
||||||
|
LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update)
|
||||||
|
LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread)
|
||||||
|
else -> throw Exception("Unknown sorting mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val comparator = if (preferences.librarySortingAscending().getOrDefault())
|
||||||
|
Comparator(sortFn)
|
||||||
|
else
|
||||||
|
Collections.reverseOrder(sortFn)
|
||||||
|
|
||||||
|
return map.mapValues { entry -> entry.value.sortedWith(comparator) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -102,7 +209,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
*
|
*
|
||||||
* @return an observable of the categories and its manga.
|
* @return an observable of the categories and its manga.
|
||||||
*/
|
*/
|
||||||
fun getLibraryObservable(): Observable<Pair<List<Category>, Map<Int, List<Manga>>>> {
|
private fun getLibraryObservable(): Observable<Pair<List<Category>, Map<Int, List<Manga>>>> {
|
||||||
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
|
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
|
||||||
{ dbCategories, libraryManga ->
|
{ dbCategories, libraryManga ->
|
||||||
val categories = if (libraryManga.containsKey(0))
|
val categories = if (libraryManga.containsKey(0))
|
||||||
@ -113,7 +220,6 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
this.categories = categories
|
this.categories = categories
|
||||||
Pair(categories, libraryManga)
|
Pair(categories, libraryManga)
|
||||||
})
|
})
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,7 +227,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
*
|
*
|
||||||
* @return an observable of the categories.
|
* @return an observable of the categories.
|
||||||
*/
|
*/
|
||||||
fun getCategoriesObservable(): Observable<List<Category>> {
|
private fun getCategoriesObservable(): Observable<List<Category>> {
|
||||||
return db.getCategories().asRxObservable()
|
return db.getCategories().asRxObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,76 +237,23 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
* @return an observable containing a map with the category id as key and a list of manga as the
|
* @return an observable containing a map with the category id as key and a list of manga as the
|
||||||
* value.
|
* value.
|
||||||
*/
|
*/
|
||||||
fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> {
|
private fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> {
|
||||||
return db.getLibraryMangas().asRxObservable()
|
return db.getLibraryMangas().asRxObservable()
|
||||||
.flatMap { mangas ->
|
.map { list -> list.groupBy { it.category } }
|
||||||
Observable.from(mangas)
|
|
||||||
// Filter library by options
|
|
||||||
.filter { filterManga(it) }
|
|
||||||
.groupBy { it.category }
|
|
||||||
.flatMap { group -> group.toList().map { Pair(group.key, it) } }
|
|
||||||
.toMap({ it.first }, { it.second })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resubscribes to library if needed.
|
* Requests the library to be filtered.
|
||||||
*/
|
*/
|
||||||
fun subscribeLibrary() {
|
fun requestFilterUpdate() {
|
||||||
if (isUnsubscribed(GET_LIBRARY)) {
|
filterTriggerRelay.call(Unit)
|
||||||
start(GET_LIBRARY)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resubscribes to library.
|
* Requests the library to be sorted.
|
||||||
*/
|
*/
|
||||||
fun resubscribeLibrary() {
|
fun requestSortUpdate() {
|
||||||
start(GET_LIBRARY)
|
sortTriggerRelay.call(Unit)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters an entry of the library.
|
|
||||||
*
|
|
||||||
* @param manga a favorite manga from the database.
|
|
||||||
* @return true if the entry is included, false otherwise.
|
|
||||||
*/
|
|
||||||
fun filterManga(manga: Manga): Boolean {
|
|
||||||
// Filter out manga without source
|
|
||||||
val source = sourceManager.get(manga.source) ?: return false
|
|
||||||
|
|
||||||
val prefFilterDownloaded = preferences.filterDownloaded().getOrDefault()
|
|
||||||
val prefFilterUnread = preferences.filterUnread().getOrDefault()
|
|
||||||
|
|
||||||
// Check if filter option is selected
|
|
||||||
if (prefFilterDownloaded || prefFilterUnread) {
|
|
||||||
|
|
||||||
// Does it have downloaded chapters.
|
|
||||||
var hasDownloaded = false
|
|
||||||
var hasUnread = false
|
|
||||||
|
|
||||||
if (prefFilterUnread) {
|
|
||||||
// Does it have unread chapters.
|
|
||||||
hasUnread = manga.unread > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prefFilterDownloaded) {
|
|
||||||
val mangaDir = downloadManager.findMangaDir(source, manga)
|
|
||||||
|
|
||||||
if (mangaDir != null) {
|
|
||||||
hasDownloaded = mangaDir.listFiles()?.any { it.isDirectory } ?: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return correct filter status
|
|
||||||
if (prefFilterDownloaded && prefFilterUnread) {
|
|
||||||
return (hasDownloaded && hasUnread)
|
|
||||||
} else {
|
|
||||||
return (hasDownloaded || hasUnread)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,7 +261,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
*/
|
*/
|
||||||
fun onOpenManga() {
|
fun onOpenManga() {
|
||||||
// Avoid further db updates for the library when it's not needed
|
// Avoid further db updates for the library when it's not needed
|
||||||
stop(GET_LIBRARY)
|
librarySubscription?.let { remove(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -220,10 +273,10 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
fun setSelection(manga: Manga, selected: Boolean) {
|
fun setSelection(manga: Manga, selected: Boolean) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selectedMangas.add(manga)
|
selectedMangas.add(manga)
|
||||||
selectionSubject.onNext(LibrarySelectionEvent.Selected(manga))
|
selectionSubject.call(LibrarySelectionEvent.Selected(manga))
|
||||||
} else {
|
} else {
|
||||||
selectedMangas.remove(manga)
|
selectedMangas.remove(manga)
|
||||||
selectionSubject.onNext(LibrarySelectionEvent.Unselected(manga))
|
selectionSubject.call(LibrarySelectionEvent.Unselected(manga))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,7 +285,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
*/
|
*/
|
||||||
fun clearSelections() {
|
fun clearSelections() {
|
||||||
selectedMangas.clear()
|
selectedMangas.clear()
|
||||||
selectionSubject.onNext(LibrarySelectionEvent.Cleared())
|
selectionSubject.call(LibrarySelectionEvent.Cleared())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -240,9 +293,12 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
*
|
*
|
||||||
* @param mangas the list of manga.
|
* @param mangas the list of manga.
|
||||||
*/
|
*/
|
||||||
fun getCommonCategories(mangas: List<Manga>): Collection<Category> = mangas.toSet()
|
fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||||
.map { db.getCategoriesForManga(it).executeAsBlocking() }
|
if (mangas.isEmpty()) return emptyList()
|
||||||
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
return mangas.toSet()
|
||||||
|
.map { db.getCategoriesForManga(it).executeAsBlocking() }
|
||||||
|
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the selected manga from the library.
|
* Remove the selected manga from the library.
|
||||||
@ -296,12 +352,4 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the active display mode.
|
|
||||||
*/
|
|
||||||
fun swapDisplayMode() {
|
|
||||||
val displayAsList = preferences.libraryAsList().getOrDefault()
|
|
||||||
preferences.libraryAsList().set(!displayAsList)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
|
object LibrarySort {
|
||||||
|
|
||||||
|
const val ALPHA = 0
|
||||||
|
const val LAST_READ = 1
|
||||||
|
const val LAST_UPDATED = 2
|
||||||
|
const val UNREAD = 3
|
||||||
|
|
||||||
|
}
|
@ -93,8 +93,12 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
val fragment = supportFragmentManager.findFragmentById(R.id.frame_container)
|
val fragment = supportFragmentManager.findFragmentById(R.id.frame_container)
|
||||||
if (fragment != null && fragment.tag.toInt() != startScreenId) {
|
if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) {
|
||||||
setSelectedDrawerItem(startScreenId)
|
drawer.closeDrawers()
|
||||||
|
} else if (fragment != null && fragment.tag.toInt() != startScreenId) {
|
||||||
|
if (resumed) {
|
||||||
|
setSelectedDrawerItem(startScreenId)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
@ -110,6 +114,8 @@ class MainActivity : BaseActivity() {
|
|||||||
} else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) {
|
} else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) {
|
||||||
// Delay activity recreation to avoid fragment leaks.
|
// Delay activity recreation to avoid fragment leaks.
|
||||||
nav_view.post { recreate() }
|
nav_view.post { recreate() }
|
||||||
|
} else if (resultCode and SettingsActivity.FLAG_LANG_CHANGED != 0) {
|
||||||
|
nav_view.post { recreate() }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
@ -136,4 +142,4 @@ class MainActivity : BaseActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val REQUEST_OPEN_SETTINGS = 200
|
private const val REQUEST_OPEN_SETTINGS = 200
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,20 @@ package eu.kanade.tachiyomi.ui.manga
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.support.graphics.drawable.VectorDrawableCompat
|
||||||
import android.support.v4.app.Fragment
|
import android.support.v4.app.Fragment
|
||||||
import android.support.v4.app.FragmentManager
|
import android.support.v4.app.FragmentManager
|
||||||
import android.support.v4.app.FragmentPagerAdapter
|
import android.support.v4.app.FragmentPagerAdapter
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
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.activity.BaseRxActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
|
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
|
||||||
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListFragment
|
import eu.kanade.tachiyomi.ui.manga.track.TrackFragment
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import kotlinx.android.synthetic.main.activity_manga.*
|
import kotlinx.android.synthetic.main.activity_manga.*
|
||||||
import kotlinx.android.synthetic.main.toolbar.*
|
import kotlinx.android.synthetic.main.toolbar.*
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
@ -27,7 +31,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
|||||||
const val FROM_LAUNCHER_EXTRA = "from_launcher"
|
const val FROM_LAUNCHER_EXTRA = "from_launcher"
|
||||||
const val INFO_FRAGMENT = 0
|
const val INFO_FRAGMENT = 0
|
||||||
const val CHAPTERS_FRAGMENT = 1
|
const val CHAPTERS_FRAGMENT = 1
|
||||||
const val MYANIMELIST_FRAGMENT = 2
|
const val TRACK_FRAGMENT = 2
|
||||||
|
|
||||||
fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent {
|
fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent {
|
||||||
SharedData.put(MangaEvent(manga))
|
SharedData.put(MangaEvent(manga))
|
||||||
@ -50,12 +54,19 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
|||||||
|
|
||||||
val fromLauncher = intent.getBooleanExtra(FROM_LAUNCHER_EXTRA, false)
|
val fromLauncher = intent.getBooleanExtra(FROM_LAUNCHER_EXTRA, false)
|
||||||
|
|
||||||
//Remove any current manga if we are launching from launcher
|
// Remove any current manga if we are launching from launcher
|
||||||
if(fromLauncher) SharedData.remove(MangaEvent::class.java)
|
if (fromLauncher) SharedData.remove(MangaEvent::class.java)
|
||||||
|
|
||||||
presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) {
|
presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) {
|
||||||
val id = intent.getLongExtra(MANGA_EXTRA, 0)
|
val id = intent.getLongExtra(MANGA_EXTRA, 0)
|
||||||
MangaEvent(presenter.db.getManga(id).executeAsBlocking()!!)
|
val dbManga = presenter.db.getManga(id).executeAsBlocking()
|
||||||
|
if (dbManga != null) {
|
||||||
|
MangaEvent(dbManga)
|
||||||
|
} else {
|
||||||
|
toast(R.string.manga_not_in_db)
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setupToolbar(toolbar)
|
setupToolbar(toolbar)
|
||||||
@ -63,6 +74,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
|||||||
fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false)
|
fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false)
|
||||||
|
|
||||||
adapter = MangaDetailAdapter(supportFragmentManager, this)
|
adapter = MangaDetailAdapter(supportFragmentManager, this)
|
||||||
|
view_pager.offscreenPageLimit = 3
|
||||||
view_pager.adapter = adapter
|
view_pager.adapter = adapter
|
||||||
|
|
||||||
tabs.setupWithViewPager(view_pager)
|
tabs.setupWithViewPager(view_pager)
|
||||||
@ -77,33 +89,50 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
|||||||
setToolbarTitle(manga.title)
|
setToolbarTitle(manga.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity) : FragmentPagerAdapter(fm) {
|
fun setTrackingIcon(visible: Boolean) {
|
||||||
|
val tab = tabs.getTabAt(TRACK_FRAGMENT) ?: return
|
||||||
|
val drawable = if (visible)
|
||||||
|
VectorDrawableCompat.create(resources, R.drawable.ic_done_white_18dp, null)
|
||||||
|
else null
|
||||||
|
|
||||||
private var pageCount: Int = 0
|
// I had no choice but to use reflection...
|
||||||
private val tabTitles = arrayOf(activity.getString(R.string.manga_detail_tab),
|
val field = tab.javaClass.getDeclaredField("mView").apply { isAccessible = true }
|
||||||
activity.getString(R.string.manga_chapters_tab), "MAL")
|
val view = field.get(tab) as LinearLayout
|
||||||
|
val textView = view.getChildAt(1) as TextView
|
||||||
|
textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
|
||||||
|
textView.compoundDrawablePadding = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity)
|
||||||
|
: FragmentPagerAdapter(fm) {
|
||||||
|
|
||||||
|
private var tabCount = 2
|
||||||
|
|
||||||
|
private val tabTitles = listOf(
|
||||||
|
R.string.manga_detail_tab,
|
||||||
|
R.string.manga_chapters_tab,
|
||||||
|
R.string.manga_tracking_tab)
|
||||||
|
.map { activity.getString(it) }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
pageCount = 2
|
if (!activity.fromCatalogue && activity.presenter.trackManager.hasLoggedServices())
|
||||||
if (!activity.fromCatalogue && activity.presenter.syncManager.myAnimeList.isLogged)
|
tabCount++
|
||||||
pageCount++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCount(): Int {
|
override fun getCount(): Int {
|
||||||
return pageCount
|
return tabCount
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItem(position: Int): Fragment? {
|
override fun getItem(position: Int): Fragment {
|
||||||
when (position) {
|
when (position) {
|
||||||
INFO_FRAGMENT -> return MangaInfoFragment.newInstance()
|
INFO_FRAGMENT -> return MangaInfoFragment.newInstance()
|
||||||
CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance()
|
CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance()
|
||||||
MYANIMELIST_FRAGMENT -> return MyAnimeListFragment.newInstance()
|
TRACK_FRAGMENT -> return TrackFragment.newInstance()
|
||||||
else -> return null
|
else -> throw Exception("Unknown position")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPageTitle(position: Int): CharSequence {
|
override fun getPageTitle(position: Int): CharSequence {
|
||||||
// Generate title based on item position
|
|
||||||
return tabTitles[position]
|
return tabTitles[position]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.manga
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
@ -22,9 +22,9 @@ class MangaPresenter : BasePresenter<MangaActivity>() {
|
|||||||
val db: DatabaseHelper by injectLazy()
|
val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manga sync manager.
|
* Tracking manager.
|
||||||
*/
|
*/
|
||||||
val syncManager: MangaSyncManager by injectLazy()
|
val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manga associated with this instance.
|
* Manga associated with this instance.
|
||||||
|
@ -118,7 +118,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
// Initialize menu items.
|
// Initialize menu items.
|
||||||
val menuFilterRead = menu.findItem(R.id.action_filter_read)
|
val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return
|
||||||
val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
|
val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
|
||||||
val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
|
val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
|
||||||
val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
|
val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
|
||||||
@ -394,7 +394,8 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun dismissDeletingDialog() {
|
fun dismissDeletingDialog() {
|
||||||
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
|
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)
|
||||||
|
?.dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onListItemClick(position: Int): Boolean {
|
override fun onListItemClick(position: Int): Boolean {
|
||||||
|
@ -1,124 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.app.DialogFragment
|
|
||||||
import android.view.View
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
|
||||||
import kotlinx.android.synthetic.main.dialog_myanimelist_search.view.*
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.subjects.PublishSubject
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class MyAnimeListDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun newInstance(): MyAnimeListDialogFragment {
|
|
||||||
return MyAnimeListDialogFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var v: View
|
|
||||||
|
|
||||||
lateinit var adapter: MyAnimeListSearchAdapter
|
|
||||||
private set
|
|
||||||
|
|
||||||
lateinit var querySubject: PublishSubject<String>
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var selectedItem: MangaSync? = null
|
|
||||||
|
|
||||||
private var searchSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
|
||||||
val dialog = MaterialDialog.Builder(activity)
|
|
||||||
.customView(R.layout.dialog_myanimelist_search, false)
|
|
||||||
.positiveText(android.R.string.ok)
|
|
||||||
.negativeText(android.R.string.cancel)
|
|
||||||
.onPositive { dialog1, which -> onPositiveButtonClick() }
|
|
||||||
.build()
|
|
||||||
|
|
||||||
onViewCreated(dialog.view, savedState)
|
|
||||||
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
|
||||||
v = view
|
|
||||||
|
|
||||||
// Create adapter
|
|
||||||
adapter = MyAnimeListSearchAdapter(activity)
|
|
||||||
view.myanimelist_search_results.adapter = adapter
|
|
||||||
|
|
||||||
// Set listeners
|
|
||||||
view.myanimelist_search_results.setOnItemClickListener { parent, viewList, position, id ->
|
|
||||||
selectedItem = adapter.getItem(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do an initial search based on the manga's title
|
|
||||||
if (savedState == null) {
|
|
||||||
val title = presenter.manga.title
|
|
||||||
view.myanimelist_search_field.append(title)
|
|
||||||
search(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
querySubject = PublishSubject.create<String>()
|
|
||||||
|
|
||||||
view.myanimelist_search_field.addTextChangedListener(object : SimpleTextWatcher() {
|
|
||||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
|
||||||
querySubject.onNext(s.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
// Listen to text changes
|
|
||||||
searchSubscription = querySubject.debounce(1, TimeUnit.SECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { search(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
searchSubscription?.unsubscribe()
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onPositiveButtonClick() {
|
|
||||||
presenter.registerManga(selectedItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun search(query: String) {
|
|
||||||
if (!query.isNullOrEmpty()) {
|
|
||||||
v.myanimelist_search_results.visibility = View.GONE
|
|
||||||
v.progress.visibility = View.VISIBLE
|
|
||||||
presenter.searchManga(query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSearchResults(results: List<MangaSync>) {
|
|
||||||
selectedItem = null
|
|
||||||
v.progress.visibility = View.GONE
|
|
||||||
v.myanimelist_search_results.visibility = View.VISIBLE
|
|
||||||
adapter.setItems(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSearchResultsError() {
|
|
||||||
v.progress.visibility = View.GONE
|
|
||||||
v.myanimelist_search_results.visibility = View.VISIBLE
|
|
||||||
adapter.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
val malFragment: MyAnimeListFragment
|
|
||||||
get() = parentFragment as MyAnimeListFragment
|
|
||||||
|
|
||||||
val presenter: MyAnimeListPresenter
|
|
||||||
get() = malFragment.presenter
|
|
||||||
|
|
||||||
}
|
|
@ -1,177 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.NumberPicker
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import kotlinx.android.synthetic.main.card_myanimelist_personal.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_myanimelist.*
|
|
||||||
import nucleus.factory.RequiresPresenter
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
|
|
||||||
@RequiresPresenter(MyAnimeListPresenter::class)
|
|
||||||
class MyAnimeListFragment : BaseRxFragment<MyAnimeListPresenter>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun newInstance(): MyAnimeListFragment {
|
|
||||||
return MyAnimeListFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var dialog: MyAnimeListDialogFragment? = null
|
|
||||||
|
|
||||||
private val decimalFormat = DecimalFormat("#.##")
|
|
||||||
|
|
||||||
private val SEARCH_FRAGMENT_TAG = "mal_search"
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_myanimelist, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
|
||||||
swipe_refresh.isEnabled = false
|
|
||||||
swipe_refresh.setOnRefreshListener { presenter.refresh() }
|
|
||||||
myanimelist_title_layout.setOnClickListener { onTitleClick() }
|
|
||||||
myanimelist_status_layout.setOnClickListener { onStatusClick() }
|
|
||||||
myanimelist_chapters_layout.setOnClickListener { onChaptersClick() }
|
|
||||||
myanimelist_score_layout.setOnClickListener { onScoreClick() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun setMangaSync(mangaSync: MangaSync?) {
|
|
||||||
swipe_refresh.isEnabled = mangaSync != null
|
|
||||||
mangaSync?.let {
|
|
||||||
myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
|
|
||||||
myanimelist_title.setAllCaps(false)
|
|
||||||
myanimelist_title.text = it.title
|
|
||||||
myanimelist_chapters.text = if (it.total_chapters > 0)
|
|
||||||
"${it.last_chapter_read}/${it.total_chapters}" else "${it.last_chapter_read}/-"
|
|
||||||
myanimelist_score.text = if (it.score == 0f) "-" else decimalFormat.format(it.score)
|
|
||||||
myanimelist_status.text = presenter.myAnimeList.getStatus(it.status)
|
|
||||||
} ?: run {
|
|
||||||
myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
|
||||||
myanimelist_title.setText(R.string.action_edit)
|
|
||||||
myanimelist_chapters.text = ""
|
|
||||||
myanimelist_score.text = ""
|
|
||||||
myanimelist_status.text = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRefreshDone() {
|
|
||||||
swipe_refresh.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRefreshError(error: Throwable) {
|
|
||||||
swipe_refresh.isRefreshing = false
|
|
||||||
context.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchResults(results: List<MangaSync>) {
|
|
||||||
findSearchFragmentIfNeeded()
|
|
||||||
|
|
||||||
dialog?.onSearchResults(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchResultsError(error: Throwable) {
|
|
||||||
findSearchFragmentIfNeeded()
|
|
||||||
context.toast(error.message)
|
|
||||||
|
|
||||||
dialog?.onSearchResultsError()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findSearchFragmentIfNeeded() {
|
|
||||||
if (dialog == null) {
|
|
||||||
dialog = childFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG) as MyAnimeListDialogFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTitleClick() {
|
|
||||||
if (dialog == null) {
|
|
||||||
dialog = MyAnimeListDialogFragment.newInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
presenter.restartSearch()
|
|
||||||
dialog?.show(childFragmentManager, SEARCH_FRAGMENT_TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onStatusClick() {
|
|
||||||
if (presenter.mangaSync == null)
|
|
||||||
return
|
|
||||||
|
|
||||||
MaterialDialog.Builder(activity)
|
|
||||||
.title(R.string.status)
|
|
||||||
.items(presenter.getAllStatus())
|
|
||||||
.itemsCallbackSingleChoice(presenter.getIndexFromStatus(), { dialog, view, i, charSequence ->
|
|
||||||
presenter.setStatus(i)
|
|
||||||
myanimelist_status.text = "..."
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onChaptersClick() {
|
|
||||||
if (presenter.mangaSync == null)
|
|
||||||
return
|
|
||||||
|
|
||||||
val dialog = MaterialDialog.Builder(activity)
|
|
||||||
.title(R.string.chapters)
|
|
||||||
.customView(R.layout.dialog_myanimelist_chapters, false)
|
|
||||||
.positiveText(android.R.string.ok)
|
|
||||||
.negativeText(android.R.string.cancel)
|
|
||||||
.onPositive { d, action ->
|
|
||||||
val view = d.customView
|
|
||||||
if (view != null) {
|
|
||||||
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
|
||||||
np.clearFocus()
|
|
||||||
presenter.setLastChapterRead(np.value)
|
|
||||||
myanimelist_chapters.text = "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
|
|
||||||
val view = dialog.customView
|
|
||||||
if (view != null) {
|
|
||||||
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
|
||||||
// Set initial value
|
|
||||||
np.value = presenter.mangaSync!!.last_chapter_read
|
|
||||||
// Don't allow to go from 0 to 9999
|
|
||||||
np.wrapSelectorWheel = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onScoreClick() {
|
|
||||||
if (presenter.mangaSync == null)
|
|
||||||
return
|
|
||||||
|
|
||||||
val dialog = MaterialDialog.Builder(activity)
|
|
||||||
.title(R.string.score)
|
|
||||||
.customView(R.layout.dialog_myanimelist_score, false)
|
|
||||||
.positiveText(android.R.string.ok)
|
|
||||||
.negativeText(android.R.string.cancel)
|
|
||||||
.onPositive { d, action ->
|
|
||||||
val view = d.customView
|
|
||||||
if (view != null) {
|
|
||||||
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
|
||||||
np.clearFocus()
|
|
||||||
presenter.setScore(np.value)
|
|
||||||
myanimelist_score.text = "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
|
|
||||||
val view = dialog.customView
|
|
||||||
if (view != null) {
|
|
||||||
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
|
||||||
// Set initial value
|
|
||||||
np.value = presenter.mangaSync!!.score.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,174 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import rx.Observable
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() {
|
|
||||||
|
|
||||||
val db: DatabaseHelper by injectLazy()
|
|
||||||
val syncManager: MangaSyncManager by injectLazy()
|
|
||||||
|
|
||||||
val myAnimeList by lazy { syncManager.myAnimeList }
|
|
||||||
|
|
||||||
lateinit var manga: Manga
|
|
||||||
private set
|
|
||||||
|
|
||||||
var mangaSync: MangaSync? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var query: String? = null
|
|
||||||
|
|
||||||
private val GET_MANGA_SYNC = 1
|
|
||||||
private val GET_SEARCH_RESULTS = 2
|
|
||||||
private val REFRESH = 3
|
|
||||||
|
|
||||||
private val PREFIX_MY = "my:"
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
startableLatestCache(GET_MANGA_SYNC,
|
|
||||||
{ db.getMangaSync(manga, myAnimeList).asRxObservable()
|
|
||||||
.doOnNext { mangaSync = it }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread()) },
|
|
||||||
{ view, mangaSync -> view.setMangaSync(mangaSync) })
|
|
||||||
|
|
||||||
startableLatestCache(GET_SEARCH_RESULTS,
|
|
||||||
{ getSearchResultsObservable() },
|
|
||||||
{ view, results -> view.setSearchResults(results) },
|
|
||||||
{ view, error -> view.setSearchResultsError(error) })
|
|
||||||
|
|
||||||
startableFirst(REFRESH,
|
|
||||||
{ getRefreshObservable() },
|
|
||||||
{ view, result -> view.onRefreshDone() },
|
|
||||||
{ view, error -> view.onRefreshError(error) })
|
|
||||||
|
|
||||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
|
||||||
start(GET_MANGA_SYNC)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSearchResultsObservable(): Observable<List<MangaSync>> {
|
|
||||||
return query?.let { query ->
|
|
||||||
val observable: Observable<List<MangaSync>>
|
|
||||||
if (query.startsWith(PREFIX_MY)) {
|
|
||||||
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
|
|
||||||
observable = myAnimeList.getList()
|
|
||||||
.flatMap { Observable.from(it) }
|
|
||||||
.filter { it.title.toLowerCase().contains(realQuery) }
|
|
||||||
.toList()
|
|
||||||
} else {
|
|
||||||
observable = myAnimeList.search(query)
|
|
||||||
}
|
|
||||||
observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
|
||||||
} ?: Observable.error(Exception("Null query"))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRefreshObservable(): Observable<PutResult> {
|
|
||||||
return mangaSync?.let { mangaSync ->
|
|
||||||
myAnimeList.getList()
|
|
||||||
.map { myList ->
|
|
||||||
myList.find { it.remote_id == mangaSync.remote_id }?.let {
|
|
||||||
mangaSync.copyPersonalFrom(it)
|
|
||||||
mangaSync.total_chapters = it.total_chapters
|
|
||||||
mangaSync
|
|
||||||
} ?: throw Exception("Could not find manga")
|
|
||||||
}
|
|
||||||
.flatMap { db.insertMangaSync(it).asRxObservable() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
} ?: Observable.error(Exception("Not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateRemote() {
|
|
||||||
mangaSync?.let { mangaSync ->
|
|
||||||
add(myAnimeList.update(mangaSync)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({ next -> },
|
|
||||||
{ error ->
|
|
||||||
Timber.e(error)
|
|
||||||
// Restart on error to set old values
|
|
||||||
start(GET_MANGA_SYNC)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun searchManga(query: String) {
|
|
||||||
if (query.isNullOrEmpty() || query == this.query)
|
|
||||||
return
|
|
||||||
|
|
||||||
this.query = query
|
|
||||||
start(GET_SEARCH_RESULTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restartSearch() {
|
|
||||||
query = null
|
|
||||||
stop(GET_SEARCH_RESULTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerManga(sync: MangaSync?) {
|
|
||||||
if (sync != null) {
|
|
||||||
sync.manga_id = manga.id!!
|
|
||||||
add(myAnimeList.bind(sync)
|
|
||||||
.flatMap { db.insertMangaSync(sync).asRxObservable() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({ },
|
|
||||||
{ error -> context.toast(error.message) }))
|
|
||||||
} else {
|
|
||||||
db.deleteMangaSyncForManga(manga).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllStatus(): List<String> {
|
|
||||||
return listOf(context.getString(R.string.reading),
|
|
||||||
context.getString(R.string.completed),
|
|
||||||
context.getString(R.string.on_hold),
|
|
||||||
context.getString(R.string.dropped),
|
|
||||||
context.getString(R.string.plan_to_read))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIndexFromStatus(): Int {
|
|
||||||
return mangaSync?.let { mangaSync ->
|
|
||||||
if (mangaSync.status == 6) 4 else mangaSync.status - 1
|
|
||||||
} ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStatus(index: Int) {
|
|
||||||
mangaSync?.status = if (index == 4) 6 else index + 1
|
|
||||||
updateRemote()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setScore(score: Int) {
|
|
||||||
mangaSync?.score = score.toFloat()
|
|
||||||
updateRemote()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLastChapterRead(chapterNumber: Int) {
|
|
||||||
mangaSync?.last_chapter_read = chapterNumber
|
|
||||||
updateRemote()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
if (mangaSync != null) {
|
|
||||||
start(REFRESH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
|
||||||
import kotlinx.android.synthetic.main.dialog_myanimelist_search_item.view.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class MyAnimeListSearchAdapter(context: Context) :
|
|
||||||
ArrayAdapter<MangaSync>(context, R.layout.dialog_myanimelist_search_item, ArrayList<MangaSync>()) {
|
|
||||||
|
|
||||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
|
||||||
var v = view
|
|
||||||
// Get the data item for this position
|
|
||||||
val sync = getItem(position)
|
|
||||||
// Check if an existing view is being reused, otherwise inflate the view
|
|
||||||
val holder: SearchViewHolder // view lookup cache stored in tag
|
|
||||||
if (v == null) {
|
|
||||||
v = parent.inflate(R.layout.dialog_myanimelist_search_item)
|
|
||||||
holder = SearchViewHolder(v)
|
|
||||||
v.tag = holder
|
|
||||||
} else {
|
|
||||||
holder = v.tag as SearchViewHolder
|
|
||||||
}
|
|
||||||
holder.onSetValues(sync)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItems(syncs: List<MangaSync>) {
|
|
||||||
setNotifyOnChange(false)
|
|
||||||
clear()
|
|
||||||
addAll(syncs)
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchViewHolder(private val view: View) {
|
|
||||||
|
|
||||||
fun onSetValues(sync: MangaSync) {
|
|
||||||
view.myanimelist_result_title.text = sync.title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
|
||||||
|
class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter<TrackHolder>() {
|
||||||
|
|
||||||
|
var items = emptyList<TrackItem>()
|
||||||
|
set(value) {
|
||||||
|
if (field !== value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var onClickListener: (TrackItem) -> Unit = {}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
|
||||||
|
val view = parent.inflate(R.layout.item_track)
|
||||||
|
return TrackHolder(view, fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: TrackHolder, position: Int) {
|
||||||
|
holder.onSetValues(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.NumberPicker
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
|
import kotlinx.android.synthetic.main.fragment_track.*
|
||||||
|
import nucleus.factory.RequiresPresenter
|
||||||
|
|
||||||
|
@RequiresPresenter(TrackPresenter::class)
|
||||||
|
class TrackFragment : BaseRxFragment<TrackPresenter>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance(): TrackFragment {
|
||||||
|
return TrackFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var adapter: TrackAdapter
|
||||||
|
|
||||||
|
private var dialog: TrackSearchDialog? = null
|
||||||
|
|
||||||
|
private val searchFragmentTag: String
|
||||||
|
get() = "search_fragment"
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_track, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
adapter = TrackAdapter(this)
|
||||||
|
recycler.layoutManager = LinearLayoutManager(context)
|
||||||
|
recycler.adapter = adapter
|
||||||
|
swipe_refresh.isEnabled = false
|
||||||
|
swipe_refresh.setOnRefreshListener { presenter.refresh() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findSearchFragmentIfNeeded() {
|
||||||
|
if (dialog == null) {
|
||||||
|
dialog = childFragmentManager.findFragmentByTag(searchFragmentTag) as TrackSearchDialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNextTrackings(trackings: List<TrackItem>) {
|
||||||
|
adapter.items = trackings
|
||||||
|
swipe_refresh.isEnabled = trackings.any { it.track != null }
|
||||||
|
(activity as MangaActivity).setTrackingIcon(trackings.any { it.track != null })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResults(results: List<Track>) {
|
||||||
|
if (!isResumed) return
|
||||||
|
|
||||||
|
findSearchFragmentIfNeeded()
|
||||||
|
dialog?.onSearchResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResultsError(error: Throwable) {
|
||||||
|
if (!isResumed) return
|
||||||
|
|
||||||
|
findSearchFragmentIfNeeded()
|
||||||
|
dialog?.onSearchResultsError()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRefreshDone() {
|
||||||
|
swipe_refresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRefreshError(error: Throwable) {
|
||||||
|
swipe_refresh.isRefreshing = false
|
||||||
|
context.toast(error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTitleClick(item: TrackItem) {
|
||||||
|
if (!isResumed) return
|
||||||
|
|
||||||
|
if (dialog == null) {
|
||||||
|
dialog = TrackSearchDialog.newInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
presenter.selectedService = item.service
|
||||||
|
dialog?.show(childFragmentManager, searchFragmentTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onStatusClick(item: TrackItem) {
|
||||||
|
if (!isResumed || item.track == null) return
|
||||||
|
|
||||||
|
val statusList = item.service.getStatusList().map { item.service.getStatus(it) }
|
||||||
|
val selectedIndex = item.service.getStatusList().indexOf(item.track.status)
|
||||||
|
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.status)
|
||||||
|
.items(statusList)
|
||||||
|
.itemsCallbackSingleChoice(selectedIndex, { dialog, view, i, charSequence ->
|
||||||
|
presenter.setStatus(item, i)
|
||||||
|
swipe_refresh.isRefreshing = true
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onChaptersClick(item: TrackItem) {
|
||||||
|
if (!isResumed || item.track == null) return
|
||||||
|
|
||||||
|
val dialog = MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.chapters)
|
||||||
|
.customView(R.layout.dialog_track_chapters, false)
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onPositive { d, action ->
|
||||||
|
val view = d.customView
|
||||||
|
if (view != null) {
|
||||||
|
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
||||||
|
np.clearFocus()
|
||||||
|
presenter.setLastChapterRead(item, np.value)
|
||||||
|
swipe_refresh.isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
|
||||||
|
val view = dialog.customView
|
||||||
|
if (view != null) {
|
||||||
|
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
||||||
|
// Set initial value
|
||||||
|
np.value = item.track.last_chapter_read
|
||||||
|
// Don't allow to go from 0 to 9999
|
||||||
|
np.wrapSelectorWheel = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onScoreClick(item: TrackItem) {
|
||||||
|
if (!isResumed || item.track == null) return
|
||||||
|
|
||||||
|
val dialog = MaterialDialog.Builder(activity)
|
||||||
|
.title(R.string.score)
|
||||||
|
.customView(R.layout.dialog_track_score, false)
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onPositive { d, action ->
|
||||||
|
val view = d.customView
|
||||||
|
if (view != null) {
|
||||||
|
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
||||||
|
np.clearFocus()
|
||||||
|
presenter.setScore(item, np.value)
|
||||||
|
swipe_refresh.isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
|
||||||
|
val view = dialog.customView
|
||||||
|
if (view != null) {
|
||||||
|
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
||||||
|
val scores = item.service.getScoreList().toTypedArray()
|
||||||
|
np.maxValue = scores.size - 1
|
||||||
|
np.displayedValues = scores
|
||||||
|
|
||||||
|
// Set initial value
|
||||||
|
val displayedScore = item.service.displayScore(item.track)
|
||||||
|
if (displayedScore != "-") {
|
||||||
|
val index = scores.indexOf(displayedScore)
|
||||||
|
np.value = if (index != -1) index else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.View
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import kotlinx.android.synthetic.main.item_track.view.*
|
||||||
|
|
||||||
|
class TrackHolder(private val view: View, private val fragment: TrackFragment)
|
||||||
|
: RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
private lateinit var item: TrackItem
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.title_container.setOnClickListener { fragment.onTitleClick(item) }
|
||||||
|
view.status_container.setOnClickListener { fragment.onStatusClick(item) }
|
||||||
|
view.chapters_container.setOnClickListener { fragment.onChaptersClick(item) }
|
||||||
|
view.score_container.setOnClickListener { fragment.onScoreClick(item) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
fun onSetValues(item: TrackItem) = with(view) {
|
||||||
|
this@TrackHolder.item = item
|
||||||
|
val track = item.track
|
||||||
|
track_logo.setImageResource(item.service.getLogo())
|
||||||
|
logo.setBackgroundColor(item.service.getLogoColor())
|
||||||
|
if (track != null) {
|
||||||
|
track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
|
||||||
|
track_title.setAllCaps(false)
|
||||||
|
track_title.text = track.title
|
||||||
|
track_chapters.text = "${track.last_chapter_read}/" +
|
||||||
|
if (track.total_chapters > 0) track.total_chapters else "-"
|
||||||
|
track_status.text = item.service.getStatus(track.status)
|
||||||
|
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
|
||||||
|
} else {
|
||||||
|
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
||||||
|
track_title.setText(R.string.action_edit)
|
||||||
|
track_chapters.text = ""
|
||||||
|
track_score.text = ""
|
||||||
|
track_status.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
|
||||||
|
class TrackItem(val track: Track?, val service: TrackService) {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||||
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class TrackPresenter : BasePresenter<TrackFragment>() {
|
||||||
|
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
lateinit var manga: Manga
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var trackList: List<TrackItem> = emptyList()
|
||||||
|
|
||||||
|
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
|
||||||
|
var selectedService: TrackService? = null
|
||||||
|
|
||||||
|
private var trackSubscription: Subscription? = null
|
||||||
|
|
||||||
|
private var searchSubscription: Subscription? = null
|
||||||
|
|
||||||
|
private var refreshSubscription: Subscription? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||||
|
fetchTrackings()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchTrackings() {
|
||||||
|
trackSubscription?.let { remove(it) }
|
||||||
|
trackSubscription = db.getTracks(manga)
|
||||||
|
.asRxObservable()
|
||||||
|
.map { tracks ->
|
||||||
|
loggedServices.map { service ->
|
||||||
|
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnNext { trackList = it }
|
||||||
|
.subscribeLatestCache(TrackFragment::onNextTrackings)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
refreshSubscription?.let { remove(it) }
|
||||||
|
refreshSubscription = Observable.from(trackList)
|
||||||
|
.filter { it.track != null }
|
||||||
|
.concatMap { item ->
|
||||||
|
item.service.refresh(item.track!!)
|
||||||
|
.flatMap { db.insertTrack(it).asRxObservable() }
|
||||||
|
.map { item }
|
||||||
|
.onErrorReturn { item }
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||||
|
TrackFragment::onRefreshError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String) {
|
||||||
|
val service = selectedService ?: return
|
||||||
|
|
||||||
|
searchSubscription?.let { remove(it) }
|
||||||
|
searchSubscription = service.search(query)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeLatestCache(TrackFragment::onSearchResults,
|
||||||
|
TrackFragment::onSearchResultsError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerTracking(item: Track?) {
|
||||||
|
val service = selectedService ?: return
|
||||||
|
|
||||||
|
if (item != null) {
|
||||||
|
item.manga_id = manga.id!!
|
||||||
|
add(service.bind(item)
|
||||||
|
.flatMap { db.insertTrack(item).asRxObservable() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ },
|
||||||
|
{ error -> context.toast(error.message) }))
|
||||||
|
} else {
|
||||||
|
db.deleteTrackForManga(manga, service).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRemote(track: Track, service: TrackService) {
|
||||||
|
service.update(track)
|
||||||
|
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||||
|
{ view, error ->
|
||||||
|
view.onRefreshError(error)
|
||||||
|
|
||||||
|
// Restart on error to set old values
|
||||||
|
fetchTrackings()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStatus(item: TrackItem, index: Int) {
|
||||||
|
val track = item.track!!
|
||||||
|
track.status = item.service.getStatusList()[index]
|
||||||
|
updateRemote(track, item.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setScore(item: TrackItem, index: Int) {
|
||||||
|
val track = item.track!!
|
||||||
|
track.score = item.service.indexToScore(index)
|
||||||
|
updateRemote(track, item.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
|
||||||
|
val track = item.track!!
|
||||||
|
track.last_chapter_read = chapterNumber
|
||||||
|
updateRemote(track, item.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import kotlinx.android.synthetic.main.item_track_search.view.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class TrackSearchAdapter(context: Context)
|
||||||
|
: ArrayAdapter<Track>(context, R.layout.item_track_search, ArrayList<Track>()) {
|
||||||
|
|
||||||
|
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||||
|
var v = view
|
||||||
|
// Get the data item for this position
|
||||||
|
val track = getItem(position)
|
||||||
|
// Check if an existing view is being reused, otherwise inflate the view
|
||||||
|
val holder: TrackSearchHolder // view lookup cache stored in tag
|
||||||
|
if (v == null) {
|
||||||
|
v = parent.inflate(R.layout.item_track_search)
|
||||||
|
holder = TrackSearchHolder(v)
|
||||||
|
v.tag = holder
|
||||||
|
} else {
|
||||||
|
holder = v.tag as TrackSearchHolder
|
||||||
|
}
|
||||||
|
holder.onSetValues(track)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItems(syncs: List<Track>) {
|
||||||
|
setNotifyOnChange(false)
|
||||||
|
clear()
|
||||||
|
addAll(syncs)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackSearchHolder(private val view: View) {
|
||||||
|
|
||||||
|
fun onSetValues(track: Track) {
|
||||||
|
view.track_search_title.text = track.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.view.View
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
||||||
|
import kotlinx.android.synthetic.main.dialog_track_search.view.*
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class TrackSearchDialog : DialogFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newInstance(): TrackSearchDialog {
|
||||||
|
return TrackSearchDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var v: View
|
||||||
|
|
||||||
|
lateinit var adapter: TrackSearchAdapter
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val queryRelay by lazy { PublishRelay.create<String>() }
|
||||||
|
|
||||||
|
private var searchDebounceSubscription: Subscription? = null
|
||||||
|
|
||||||
|
private var selectedItem: Track? = null
|
||||||
|
|
||||||
|
val presenter: TrackPresenter
|
||||||
|
get() = (parentFragment as TrackFragment).presenter
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||||
|
val dialog = MaterialDialog.Builder(context)
|
||||||
|
.customView(R.layout.dialog_track_search, false)
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onPositive { dialog1, which -> onPositiveButtonClick() }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
onViewCreated(dialog.view, savedState)
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
|
v = view
|
||||||
|
|
||||||
|
// Create adapter
|
||||||
|
adapter = TrackSearchAdapter(context)
|
||||||
|
view.track_search_list.adapter = adapter
|
||||||
|
|
||||||
|
// Set listeners
|
||||||
|
selectedItem = null
|
||||||
|
view.track_search_list.setOnItemClickListener { parent, viewList, position, id ->
|
||||||
|
selectedItem = adapter.getItem(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do an initial search based on the manga's title
|
||||||
|
if (savedState == null) {
|
||||||
|
val title = presenter.manga.title
|
||||||
|
view.track_search.append(title)
|
||||||
|
search(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.track_search.addTextChangedListener(object : SimpleTextWatcher() {
|
||||||
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||||
|
queryRelay.call(s.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Listen to text changes
|
||||||
|
searchDebounceSubscription = queryRelay.debounce(1, TimeUnit.SECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.subscribe { search(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
searchDebounceSubscription?.unsubscribe()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun search(query: String) {
|
||||||
|
v.progress.visibility = View.VISIBLE
|
||||||
|
v.track_search_list.visibility = View.GONE
|
||||||
|
|
||||||
|
presenter.search(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResults(results: List<Track>) {
|
||||||
|
selectedItem = null
|
||||||
|
v.progress.visibility = View.GONE
|
||||||
|
v.track_search_list.visibility = View.VISIBLE
|
||||||
|
adapter.setItems(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResultsError() {
|
||||||
|
v.progress.visibility = View.VISIBLE
|
||||||
|
v.track_search_list.visibility = View.GONE
|
||||||
|
adapter.setItems(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPositiveButtonClick() {
|
||||||
|
presenter.registerTracking(selectedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -163,19 +163,19 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
val chapterToUpdate = presenter.getMangaSyncChapterToUpdate()
|
val chapterToUpdate = presenter.getTrackChapterToUpdate()
|
||||||
|
|
||||||
if (chapterToUpdate > 0) {
|
if (chapterToUpdate > 0) {
|
||||||
if (preferences.askUpdateMangaSync()) {
|
if (preferences.askUpdateTrack()) {
|
||||||
MaterialDialog.Builder(this)
|
MaterialDialog.Builder(this)
|
||||||
.content(getString(R.string.confirm_update_manga_sync, chapterToUpdate))
|
.content(getString(R.string.confirm_update_manga_sync, chapterToUpdate))
|
||||||
.positiveText(android.R.string.yes)
|
.positiveText(android.R.string.yes)
|
||||||
.negativeText(android.R.string.no)
|
.negativeText(android.R.string.no)
|
||||||
.onPositive { dialog, which -> presenter.updateMangaSyncLastChapterRead() }
|
.onPositive { dialog, which -> presenter.updateTrackLastChapterRead() }
|
||||||
.onAny { dialog1, which1 -> super.onBackPressed() }
|
.onAny { dialog1, which1 -> super.onBackPressed() }
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
presenter.updateMangaSyncLastChapterRead()
|
presenter.updateTrackLastChapterRead()
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -189,7 +189,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||||
if (volumeKeysEnabled) {
|
if (volumeKeysEnabled) {
|
||||||
if (event.action == KeyEvent.ACTION_UP) {
|
if (event.action == KeyEvent.ACTION_UP) {
|
||||||
viewer?.moveToNext()
|
viewer?.moveDown()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -197,7 +197,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||||
if (volumeKeysEnabled) {
|
if (volumeKeysEnabled) {
|
||||||
if (event.action == KeyEvent.ACTION_UP) {
|
if (event.action == KeyEvent.ACTION_UP) {
|
||||||
viewer?.moveToPrevious()
|
viewer?.moveUp()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -210,12 +210,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (!isFinishing) {
|
if (!isFinishing) {
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
KeyEvent.KEYCODE_DPAD_RIGHT -> viewer?.moveToNext()
|
KeyEvent.KEYCODE_DPAD_RIGHT -> viewer?.moveRight()
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT -> viewer?.moveToPrevious()
|
KeyEvent.KEYCODE_DPAD_LEFT -> viewer?.moveLeft()
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN -> viewer?.moveDown()
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP -> viewer?.moveUp()
|
||||||
KeyEvent.KEYCODE_MENU -> toggleMenu()
|
KeyEvent.KEYCODE_MENU -> toggleMenu()
|
||||||
|
else -> return super.onKeyUp(keyCode, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.onKeyUp(keyCode, event)
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onChapterError(error: Throwable) {
|
fun onChapterError(error: Throwable) {
|
||||||
@ -224,14 +227,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
toast(error.message)
|
toast(error.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLongPress(page: Page) {
|
fun onLongClick(page: Page) {
|
||||||
MaterialDialog.Builder(this)
|
MaterialDialog.Builder(this)
|
||||||
.title(getString(R.string.options))
|
.title(getString(R.string.options))
|
||||||
.items(R.array.reader_image_options)
|
.items(R.array.reader_image_options)
|
||||||
.itemsIds(R.array.reader_image_options_values)
|
.itemsIds(R.array.reader_image_options_values)
|
||||||
.itemsCallback { materialDialog, view, i, charSequence ->
|
.itemsCallback { materialDialog, view, i, charSequence ->
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> presenter.setCover(page)
|
0 -> setImageAsCover(page)
|
||||||
1 -> shareImage(page)
|
1 -> shareImage(page)
|
||||||
2 -> presenter.savePage(page)
|
2 -> presenter.savePage(page)
|
||||||
}
|
}
|
||||||
@ -313,14 +316,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
val mangaViewer = if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
|
val mangaViewer = if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
|
||||||
|
|
||||||
// Try to reuse the viewer using its tag
|
// Try to reuse the viewer using its tag
|
||||||
var fragment: BaseReader? = supportFragmentManager.findFragmentByTag(manga.viewer.toString()) as? BaseReader
|
var fragment = supportFragmentManager.findFragmentByTag(manga.viewer.toString()) as? BaseReader
|
||||||
if (fragment == null) {
|
if (fragment == null) {
|
||||||
// Create a new viewer
|
// Create a new viewer
|
||||||
when (mangaViewer) {
|
fragment = when (mangaViewer) {
|
||||||
RIGHT_TO_LEFT -> fragment = RightToLeftReader()
|
RIGHT_TO_LEFT -> RightToLeftReader()
|
||||||
VERTICAL -> fragment = VerticalReader()
|
VERTICAL -> VerticalReader()
|
||||||
WEBTOON -> fragment = WebtoonReader()
|
WEBTOON -> WebtoonReader()
|
||||||
else -> fragment = LeftToRightReader()
|
else -> LeftToRightReader()
|
||||||
}
|
}
|
||||||
|
|
||||||
supportFragmentManager.beginTransaction().replace(R.id.reader, fragment, manga.viewer.toString()).commit()
|
supportFragmentManager.beginTransaction().replace(R.id.reader, fragment, manga.viewer.toString()).commit()
|
||||||
@ -469,23 +472,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a share intent that lets user share image
|
|
||||||
*
|
|
||||||
* @param page page object containing image information.
|
|
||||||
*/
|
|
||||||
fun shareImage(page: Page) {
|
|
||||||
if (page.status != Page.READY)
|
|
||||||
return
|
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
putExtra(Intent.EXTRA_STREAM, page.uri)
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
type = "image/*"
|
|
||||||
}
|
|
||||||
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the brightness of the screen. Range is [-75, 100].
|
* Sets the brightness of the screen. Range is [-75, 100].
|
||||||
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
|
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
|
||||||
@ -570,4 +556,39 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a share intent that lets user share image
|
||||||
|
*
|
||||||
|
* @param page page object containing image information.
|
||||||
|
*/
|
||||||
|
private fun shareImage(page: Page) {
|
||||||
|
if (page.status != Page.READY)
|
||||||
|
return
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
putExtra(Intent.EXTRA_STREAM, page.uri)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
type = "image/*"
|
||||||
|
}
|
||||||
|
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the given page as the cover of the manga.
|
||||||
|
*
|
||||||
|
* @param page the page containing the image to set as cover.
|
||||||
|
*/
|
||||||
|
private fun setImageAsCover(page: Page) {
|
||||||
|
if (page.status != Page.READY)
|
||||||
|
return
|
||||||
|
|
||||||
|
MaterialDialog.Builder(this)
|
||||||
|
.content(getString(R.string.confirm_set_image_as_cover))
|
||||||
|
.positiveText(android.R.string.yes)
|
||||||
|
.negativeText(android.R.string.no)
|
||||||
|
.onPositive { dialog, which -> presenter.setImageAsCover(page) }
|
||||||
|
.show()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,14 +10,14 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackUpdateService
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier
|
import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
@ -54,9 +54,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
val downloadManager: DownloadManager by injectLazy()
|
val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync manager.
|
* Tracking manager.
|
||||||
*/
|
*/
|
||||||
val syncManager: MangaSyncManager by injectLazy()
|
val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source manager.
|
* Source manager.
|
||||||
@ -124,7 +124,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
/**
|
/**
|
||||||
* List of manga services linked to the active manga, or null if auto syncing is not enabled.
|
* List of manga services linked to the active manga, or null if auto syncing is not enabled.
|
||||||
*/
|
*/
|
||||||
private var mangaSyncList: List<MangaSync>? = null
|
private var trackList: List<Track>? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chapter loader whose job is to obtain the chapter list and initialize every page.
|
* Chapter loader whose job is to obtain the chapter list and initialize every page.
|
||||||
@ -141,6 +141,11 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
*/
|
*/
|
||||||
private var adjacentChaptersSubscription: Subscription? = null
|
private var adjacentChaptersSubscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the active chapter has been loaded.
|
||||||
|
*/
|
||||||
|
private var chapterLoaded = false
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Id of the restartable that loads the active chapter.
|
* Id of the restartable that loads the active chapter.
|
||||||
@ -165,9 +170,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
.subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) })
|
.subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) })
|
||||||
|
|
||||||
// Retrieve the sync list if auto syncing is enabled.
|
// Retrieve the sync list if auto syncing is enabled.
|
||||||
if (prefs.autoUpdateMangaSync()) {
|
if (prefs.autoUpdateTrack()) {
|
||||||
add(db.getMangasSync(manga).asRxSingle()
|
add(db.getTracks(manga).asRxSingle()
|
||||||
.subscribe({ mangaSyncList = it }))
|
.subscribe({ trackList = it }))
|
||||||
}
|
}
|
||||||
|
|
||||||
restartableLatestCache(LOAD_ACTIVE_CHAPTER,
|
restartableLatestCache(LOAD_ACTIVE_CHAPTER,
|
||||||
@ -211,6 +216,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
return loader.loadChapter(chapter)
|
return loader.loadChapter(chapter)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnNext { chapterLoaded = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -298,6 +304,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
nextChapter = null
|
nextChapter = null
|
||||||
prevChapter = null
|
prevChapter = null
|
||||||
|
|
||||||
|
chapterLoaded = false
|
||||||
start(LOAD_ACTIVE_CHAPTER)
|
start(LOAD_ACTIVE_CHAPTER)
|
||||||
getAdjacentChapters(chapter)
|
getAdjacentChapters(chapter)
|
||||||
}
|
}
|
||||||
@ -431,9 +438,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
/**
|
/**
|
||||||
* Returns the chapter to be marked as last read in sync services or 0 if no update required.
|
* Returns the chapter to be marked as last read in sync services or 0 if no update required.
|
||||||
*/
|
*/
|
||||||
fun getMangaSyncChapterToUpdate(): Int {
|
fun getTrackChapterToUpdate(): Int {
|
||||||
val mangaSyncList = mangaSyncList
|
val trackList = trackList
|
||||||
if (chapter.pages == null || mangaSyncList == null || mangaSyncList.isEmpty())
|
if (chapter.pages == null || trackList == null || trackList.isEmpty())
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
val prevChapter = prevChapter
|
val prevChapter = prevChapter
|
||||||
@ -446,24 +453,24 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
else
|
else
|
||||||
0
|
0
|
||||||
|
|
||||||
mangaSyncList.forEach { sync ->
|
trackList.forEach { sync ->
|
||||||
if (lastChapterRead > sync.last_chapter_read) {
|
if (lastChapterRead > sync.last_chapter_read) {
|
||||||
sync.last_chapter_read = lastChapterRead
|
sync.last_chapter_read = lastChapterRead
|
||||||
sync.update = true
|
sync.update = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (mangaSyncList.any { it.update }) lastChapterRead else 0
|
return if (trackList.any { it.update }) lastChapterRead else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the service that updates the last chapter read in sync services
|
* Starts the service that updates the last chapter read in sync services
|
||||||
*/
|
*/
|
||||||
fun updateMangaSyncLastChapterRead() {
|
fun updateTrackLastChapterRead() {
|
||||||
mangaSyncList?.forEach { sync ->
|
trackList?.forEach { sync ->
|
||||||
val service = syncManager.getService(sync.sync_id)
|
val service = trackManager.getService(sync.sync_id)
|
||||||
if (service != null && service.isLogged && sync.update) {
|
if (service != null && service.isLogged && sync.update) {
|
||||||
UpdateMangaSyncService.start(context, sync)
|
TrackUpdateService.start(context, sync)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -474,6 +481,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
* @return true if the next chapter is being loaded, false if there is no next chapter.
|
* @return true if the next chapter is being loaded, false if there is no next chapter.
|
||||||
*/
|
*/
|
||||||
fun loadNextChapter(): Boolean {
|
fun loadNextChapter(): Boolean {
|
||||||
|
// Avoid skipping chapters.
|
||||||
|
if (!chapterLoaded) return true
|
||||||
|
|
||||||
nextChapter?.let {
|
nextChapter?.let {
|
||||||
onChapterLeft()
|
onChapterLeft()
|
||||||
loadChapter(it, 0)
|
loadChapter(it, 0)
|
||||||
@ -488,6 +498,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
* @return true if the previous chapter is being loaded, false if there is no previous chapter.
|
* @return true if the previous chapter is being loaded, false if there is no previous chapter.
|
||||||
*/
|
*/
|
||||||
fun loadPreviousChapter(): Boolean {
|
fun loadPreviousChapter(): Boolean {
|
||||||
|
// Avoid skipping chapters.
|
||||||
|
if (!chapterLoaded) return true
|
||||||
|
|
||||||
prevChapter?.let {
|
prevChapter?.let {
|
||||||
onChapterLeft()
|
onChapterLeft()
|
||||||
loadChapter(it, if (it.read) -1 else 0)
|
loadChapter(it, if (it.read) -1 else 0)
|
||||||
@ -523,19 +536,13 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
/**
|
/**
|
||||||
* Update cover with page file.
|
* Update cover with page file.
|
||||||
*/
|
*/
|
||||||
internal fun setCover(page: Page) {
|
internal fun setImageAsCover(page: Page) {
|
||||||
if (page.status != Page.READY)
|
|
||||||
return
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
if (manga.thumbnail_url != null) {
|
val input = context.contentResolver.openInputStream(page.uri)
|
||||||
val input = context.contentResolver.openInputStream(page.uri)
|
coverCache.copyToCache(thumbUrl, input)
|
||||||
coverCache.copyToCache(manga.thumbnail_url!!, input)
|
context.toast(R.string.cover_updated)
|
||||||
context.toast(R.string.cover_updated)
|
|
||||||
} else {
|
|
||||||
throw Exception("Image url not found")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
context.toast(R.string.notification_first_add_to_library)
|
context.toast(R.string.notification_first_add_to_library)
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,11 @@ import android.app.PendingIntent
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.support.v4.content.FileProvider
|
import android.support.v4.content.FileProvider
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import eu.kanade.tachiyomi.Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID as defaultNotification
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The BroadcastReceiver of [ImageNotifier]
|
* The BroadcastReceiver of [ImageNotifier]
|
||||||
@ -18,21 +17,16 @@ import java.io.File
|
|||||||
class ImageNotificationReceiver : BroadcastReceiver() {
|
class ImageNotificationReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
ACTION_SHARE_IMAGE -> {
|
|
||||||
shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION))
|
|
||||||
context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, 5))
|
|
||||||
}
|
|
||||||
ACTION_SHOW_IMAGE ->
|
|
||||||
showImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION))
|
|
||||||
ACTION_DELETE_IMAGE -> {
|
ACTION_DELETE_IMAGE -> {
|
||||||
deleteImage(intent.getStringExtra(EXTRA_FILE_LOCATION))
|
deleteImage(intent.getStringExtra(EXTRA_FILE_LOCATION))
|
||||||
context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, 5))
|
context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, defaultNotification))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called to delete image
|
* Called to delete image
|
||||||
|
*
|
||||||
* @param path path of file
|
* @param path path of file
|
||||||
*/
|
*/
|
||||||
private fun deleteImage(path: String) {
|
private fun deleteImage(path: String) {
|
||||||
@ -40,60 +34,42 @@ class ImageNotificationReceiver : BroadcastReceiver() {
|
|||||||
if (file.exists()) file.delete()
|
if (file.exists()) file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to start share intent to share image
|
|
||||||
* @param context context of application
|
|
||||||
* @param path path of file
|
|
||||||
*/
|
|
||||||
private fun shareImage(context: Context, path: String) {
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
|
|
||||||
putExtra(Intent.EXTRA_STREAM, Uri.parse(path))
|
|
||||||
type = "image/*"
|
|
||||||
}
|
|
||||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.action_share)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to show image in gallery application
|
|
||||||
* @param context context of application
|
|
||||||
* @param path path of file
|
|
||||||
*/
|
|
||||||
private fun showImage(context: Context, path: String) {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", File(path))
|
|
||||||
setDataAndType(uri, "image/*")
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ACTION_SHARE_IMAGE = "eu.kanade.SHARE_IMAGE"
|
|
||||||
|
|
||||||
private const val ACTION_SHOW_IMAGE = "eu.kanade.SHOW_IMAGE"
|
|
||||||
|
|
||||||
private const val ACTION_DELETE_IMAGE = "eu.kanade.DELETE_IMAGE"
|
private const val ACTION_DELETE_IMAGE = "eu.kanade.DELETE_IMAGE"
|
||||||
|
|
||||||
private const val EXTRA_FILE_LOCATION = "file_location"
|
private const val EXTRA_FILE_LOCATION = "file_location"
|
||||||
|
|
||||||
private const val NOTIFICATION_ID = "notification_id"
|
private const val NOTIFICATION_ID = "notification_id"
|
||||||
|
|
||||||
internal fun shareImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
|
/**
|
||||||
val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
|
* Called to start share intent to share image
|
||||||
action = ACTION_SHARE_IMAGE
|
*
|
||||||
putExtra(EXTRA_FILE_LOCATION, path)
|
* @param context context of application
|
||||||
putExtra(NOTIFICATION_ID, notificationId)
|
* @param file file that contains image
|
||||||
|
*/
|
||||||
|
internal fun shareImageIntent(context: Context, file: File): PendingIntent {
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
type = "image/*"
|
||||||
|
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun showImageIntent(context: Context, path: String): PendingIntent {
|
/**
|
||||||
val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
|
* Called to show image in gallery application
|
||||||
action = ACTION_SHOW_IMAGE
|
*
|
||||||
putExtra(EXTRA_FILE_LOCATION, path)
|
* @param context context of application
|
||||||
|
* @param file file that contains image
|
||||||
|
*/
|
||||||
|
internal fun showImageIntent(context: Context, file: File): PendingIntent {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
||||||
|
setDataAndType(uri, "image/*")
|
||||||
|
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun deleteImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
|
internal fun deleteImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
|
||||||
|
@ -58,11 +58,11 @@ class ImageNotifier(private val context: Context) {
|
|||||||
if (!mActions.isEmpty())
|
if (!mActions.isEmpty())
|
||||||
mActions.clear()
|
mActions.clear()
|
||||||
|
|
||||||
setContentIntent(ImageNotificationReceiver.showImageIntent(context, file.absolutePath))
|
setContentIntent(ImageNotificationReceiver.showImageIntent(context, file))
|
||||||
// Share action
|
// Share action
|
||||||
addAction(R.drawable.ic_share_grey_24dp,
|
addAction(R.drawable.ic_share_grey_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
ImageNotificationReceiver.shareImageIntent(context, file.absolutePath, notificationId))
|
ImageNotificationReceiver.shareImageIntent(context, file))
|
||||||
// Delete action
|
// Delete action
|
||||||
addAction(R.drawable.ic_delete_grey_24dp,
|
addAction(R.drawable.ic_delete_grey_24dp,
|
||||||
context.getString(R.string.action_delete),
|
context.getString(R.string.action_delete),
|
||||||
|
@ -189,14 +189,38 @@ abstract class BaseReader : BaseFragment() {
|
|||||||
abstract fun onChapterAppended(chapter: ReaderChapter)
|
abstract fun onChapterAppended(chapter: ReaderChapter)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves pages forward. Implementations decide how to move (by a page, by some distance...).
|
* Moves pages to right. Implementations decide how to move (by a page, by some distance...).
|
||||||
*/
|
*/
|
||||||
abstract fun moveToNext()
|
abstract fun moveRight()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves pages backward. Implementations decide how to move (by a page, by some distance...).
|
* Moves pages to left. Implementations decide how to move (by a page, by some distance...).
|
||||||
*/
|
*/
|
||||||
abstract fun moveToPrevious()
|
abstract fun moveLeft()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves pages down. Implementations decide how to move (by a page, by some distance...).
|
||||||
|
*/
|
||||||
|
open fun moveDown() {
|
||||||
|
moveRight()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves pages up. Implementations decide how to move (by a page, by some distance...).
|
||||||
|
*/
|
||||||
|
open fun moveUp() {
|
||||||
|
moveLeft()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method the implementations can call to show a menu with options for the given page.
|
||||||
|
*/
|
||||||
|
fun onLongClick(page: Page?): Boolean {
|
||||||
|
if (isAdded && page != null) {
|
||||||
|
readerActivity.onLongClick(page)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the active decoder class.
|
* Sets the active decoder class.
|
||||||
|
@ -71,6 +71,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
setBitmapDecoderClass(reader.bitmapDecoderClass)
|
setBitmapDecoderClass(reader.bitmapDecoderClass)
|
||||||
setVerticalScrollingParent(reader is VerticalReader)
|
setVerticalScrollingParent(reader is VerticalReader)
|
||||||
setOnTouchListener { v, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) }
|
setOnTouchListener { v, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) }
|
||||||
|
setOnLongClickListener { reader.onLongClick(page) }
|
||||||
setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||||
override fun onReady() {
|
override fun onReady() {
|
||||||
onImageDecoded(reader)
|
onImageDecoded(reader)
|
||||||
|
@ -66,7 +66,7 @@ abstract class PagerReader : BaseReader() {
|
|||||||
/**
|
/**
|
||||||
* Gesture detector for touch events.
|
* Gesture detector for touch events.
|
||||||
*/
|
*/
|
||||||
val gestureDetector by lazy { createGestureDetector() }
|
val gestureDetector by lazy { GestureDetector(context, ImageGestureListener()) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscriptions for reader settings.
|
* Subscriptions for reader settings.
|
||||||
@ -166,37 +166,24 @@ abstract class PagerReader : BaseReader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the gesture detector for the pager.
|
* Gesture detector for Subsampling Scale Image View.
|
||||||
*
|
|
||||||
* @return a gesture detector.
|
|
||||||
*/
|
*/
|
||||||
protected fun createGestureDetector(): GestureDetector {
|
inner class ImageGestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||||
return GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
|
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
|
||||||
if (isAdded) {
|
|
||||||
val positionX = e.x
|
|
||||||
|
|
||||||
if (positionX < pager.width * LEFT_REGION) {
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
if (tappingEnabled) onLeftSideTap()
|
if (isAdded) {
|
||||||
} else if (positionX > pager.width * RIGHT_REGION) {
|
val positionX = e.x
|
||||||
if (tappingEnabled) onRightSideTap()
|
|
||||||
} else {
|
|
||||||
readerActivity.toggleMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongPress(e: MotionEvent?) {
|
if (positionX < pager.width * LEFT_REGION) {
|
||||||
if (isAdded) {
|
if (tappingEnabled) moveLeft()
|
||||||
val page = adapter.pages.getOrNull(pager.currentItem)
|
} else if (positionX > pager.width * RIGHT_REGION) {
|
||||||
if (page != null)
|
if (tappingEnabled) moveRight()
|
||||||
readerActivity.onLongPress(page)
|
} else {
|
||||||
else
|
readerActivity.toggleMenu()
|
||||||
context.toast(getString(R.string.unknown_error))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -258,23 +245,23 @@ abstract class PagerReader : BaseReader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the left side of the screen was clicked.
|
* Moves a page to the right.
|
||||||
*/
|
*/
|
||||||
protected open fun onLeftSideTap() {
|
override fun moveRight() {
|
||||||
moveToPrevious()
|
moveToNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the right side of the screen was clicked.
|
* Moves a page to the left.
|
||||||
*/
|
*/
|
||||||
protected open fun onRightSideTap() {
|
override fun moveLeft() {
|
||||||
moveToNext()
|
moveToPrevious()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves to the next page or requests the next chapter if it's the last one.
|
* Moves to the next page or requests the next chapter if it's the last one.
|
||||||
*/
|
*/
|
||||||
override fun moveToNext() {
|
protected fun moveToNext() {
|
||||||
if (pager.currentItem != pager.adapter.count - 1) {
|
if (pager.currentItem != pager.adapter.count - 1) {
|
||||||
pager.setCurrentItem(pager.currentItem + 1, transitions)
|
pager.setCurrentItem(pager.currentItem + 1, transitions)
|
||||||
} else {
|
} else {
|
||||||
@ -285,7 +272,7 @@ abstract class PagerReader : BaseReader() {
|
|||||||
/**
|
/**
|
||||||
* Moves to the previous page or requests the previous chapter if it's the first one.
|
* Moves to the previous page or requests the previous chapter if it's the first one.
|
||||||
*/
|
*/
|
||||||
override fun moveToPrevious() {
|
protected fun moveToPrevious() {
|
||||||
if (pager.currentItem != 0) {
|
if (pager.currentItem != 0) {
|
||||||
pager.setCurrentItem(pager.currentItem - 1, transitions)
|
pager.setCurrentItem(pager.currentItem - 1, transitions)
|
||||||
} else {
|
} else {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user