Merge remote-tracking branch 'upstream/master'
# Conflicts: # README.md # app/build.gradle # app/src/main/java/eu/kanade/tachiyomi/App.kt # app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt # app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt # app/src/main/res/menu/library.xml # app/src/main/res/values/strings.xml # app/src/test/java/eu/kanade/tachiyomi/data/database/ChapterRecognitionTest.kt
@ -40,8 +40,8 @@ android {
|
|||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 26
|
targetSdkVersion 26
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 6600
|
versionCode 6800
|
||||||
versionName "v6.6.0-EH"
|
versionName "v6.8.0-EH"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@ -198,7 +198,8 @@ dependencies {
|
|||||||
// UI
|
// UI
|
||||||
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||||
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
implementation 'eu.davidea:flexible-adapter:5.0.0-rc3'
|
implementation 'eu.davidea:flexible-adapter:5.0.0-rc4'
|
||||||
|
implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b1'
|
||||||
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
||||||
implementation 'com.github.amulyakhare:TextDrawable:558677e'
|
implementation 'com.github.amulyakhare:TextDrawable:558677e'
|
||||||
implementation('com.afollestad.material-dialogs:core:0.9.4.7') {
|
implementation('com.afollestad.material-dialogs:core:0.9.4.7') {
|
||||||
@ -207,6 +208,7 @@ dependencies {
|
|||||||
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
||||||
implementation 'com.github.mthli:Slice:v1.2'
|
implementation 'com.github.mthli:Slice:v1.2'
|
||||||
|
implementation 'me.gujun.android.taggroup:library:1.4@aar'
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation "com.bluelinelabs:conductor:2.1.4"
|
implementation "com.bluelinelabs:conductor:2.1.4"
|
||||||
|
34
app/src/debug/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108.0"
|
||||||
|
android:viewportHeight="108.0">
|
||||||
|
<path
|
||||||
|
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#455A64"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#607D8B"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#CE2828"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#FFF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#000"/>
|
||||||
|
</vector>
|
5
app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@android:color/transparent"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@android:color/transparent"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 18 KiB |
@ -91,7 +91,7 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.updater.UpdateDownloaderService"
|
android:name=".data.updater.UpdaterService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@ -9,7 +9,7 @@ import com.github.ajalt.reprint.core.Reprint
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
@ -24,11 +24,11 @@ open class App : Application() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
Injekt = InjektScope(DefaultRegistrar())
|
Injekt = InjektScope(DefaultRegistrar())
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
|
||||||
|
|
||||||
setupJobManager()
|
setupJobManager()
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
setupRealm() //Setup metadata DB (EH)
|
setupRealm() //Setup metadata DB (EH)
|
||||||
@ -53,7 +53,7 @@ open class App : Application() {
|
|||||||
JobManager.create(this).addJobCreator { tag ->
|
JobManager.create(this).addJobCreator { tag ->
|
||||||
when (tag) {
|
when (tag) {
|
||||||
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
||||||
UpdateCheckerJob.TAG -> UpdateCheckerJob()
|
UpdaterJob.TAG -> UpdaterJob()
|
||||||
BackupCreatorJob.TAG -> BackupCreatorJob()
|
BackupCreatorJob.TAG -> BackupCreatorJob()
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object Migrations {
|
object Migrations {
|
||||||
@ -25,7 +25,7 @@ object Migrations {
|
|||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
// Restore jobs after upgrading to evernote's job scheduler.
|
// Restore jobs after upgrading to evernote's job scheduler.
|
||||||
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
||||||
UpdateCheckerJob.setupTask()
|
UpdaterJob.setupTask()
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask()
|
LibraryUpdateJob.setupTask()
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider
|
|||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
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.MangaFavoritePutResolver
|
||||||
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.resolvers.MangaLastUpdatedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
@ -74,6 +75,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaFavorite(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaFavoritePutResolver())
|
||||||
|
.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()
|
||||||
|
@ -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 MangaFavoritePutResolver : 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_FAVORITE, manga.favorite)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.notification
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.getUriCompat
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -43,11 +44,10 @@ object NotificationHandler {
|
|||||||
* Returns [PendingIntent] that prompts user with apk install intent
|
* Returns [PendingIntent] that prompts user with apk install intent
|
||||||
*
|
*
|
||||||
* @param context context
|
* @param context context
|
||||||
* @param file file of apk that is installed
|
* @param uri uri of apk that is installed
|
||||||
*/
|
*/
|
||||||
fun installApkPendingActivity(context: Context, file: File): PendingIntent {
|
fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
val uri = file.getUriCompat(context)
|
|
||||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val enableTransitions = "pref_enable_transitions_key"
|
const val enableTransitions = "pref_enable_transitions_key"
|
||||||
|
|
||||||
|
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
|
||||||
|
|
||||||
const val showPageNumber = "pref_show_page_number_key"
|
const val showPageNumber = "pref_show_page_number_key"
|
||||||
|
|
||||||
const val fullscreen = "fullscreen"
|
const val fullscreen = "fullscreen"
|
||||||
|
@ -40,6 +40,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true)
|
fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true)
|
||||||
|
|
||||||
|
fun doubleTapAnimSpeed() = rxPrefs.getInteger(Keys.doubleTapAnimationSpeed, 500)
|
||||||
|
|
||||||
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true)
|
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true)
|
||||||
|
|
||||||
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true)
|
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true)
|
||||||
@ -166,7 +168,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
//TODO
|
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
|
||||||
|
|
||||||
// --> EH
|
// --> EH
|
||||||
fun enableExhentai() = rxPrefs.getBoolean("enable_exhentai", false)
|
fun enableExhentai() = rxPrefs.getBoolean("enable_exhentai", false)
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@ import com.google.gson.annotations.SerializedName
|
|||||||
* @param assets assets of latest release.
|
* @param assets assets of latest release.
|
||||||
*/
|
*/
|
||||||
class GithubRelease(@SerializedName("tag_name") val version: String,
|
class GithubRelease(@SerializedName("tag_name") val version: String,
|
||||||
@SerializedName("body") val changeLog: String,
|
@SerializedName("body") val changeLog: String,
|
||||||
@SerializedName("assets") val assets: List<Assets>) {
|
@SerializedName("assets") private val assets: List<Assets>) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get download link of latest release from the assets.
|
* Get download link of latest release from the assets.
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.updater
|
|||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class GithubUpdateChecker() {
|
class GithubUpdateChecker {
|
||||||
|
|
||||||
private val service: GithubService = GithubService.create()
|
private val service: GithubService = GithubService.create()
|
||||||
|
|
||||||
|
@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.data.updater
|
|||||||
sealed class GithubUpdateResult {
|
sealed class GithubUpdateResult {
|
||||||
|
|
||||||
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
|
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
|
||||||
class NoNewUpdate(): GithubUpdateResult()
|
class NoNewUpdate : GithubUpdateResult()
|
||||||
}
|
}
|
@ -1,147 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.support.v4.app.NotificationCompat
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
|
||||||
import java.io.File
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Local [BroadcastReceiver] that runs on UI thread
|
|
||||||
* Notification calls from [UpdateDownloaderService] should be made from here.
|
|
||||||
*/
|
|
||||||
internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceiver() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val NAME = "UpdateDownloaderReceiver"
|
|
||||||
|
|
||||||
// Called to show initial notification.
|
|
||||||
internal const val NOTIFICATION_UPDATER_INITIAL = "$ID.$NAME.UPDATER_INITIAL"
|
|
||||||
|
|
||||||
// Called to show progress notification.
|
|
||||||
internal const val NOTIFICATION_UPDATER_PROGRESS = "$ID.$NAME.UPDATER_PROGRESS"
|
|
||||||
|
|
||||||
// Called to show install notification.
|
|
||||||
internal const val NOTIFICATION_UPDATER_INSTALL = "$ID.$NAME.UPDATER_INSTALL"
|
|
||||||
|
|
||||||
// Called to show error notification
|
|
||||||
internal const val NOTIFICATION_UPDATER_ERROR = "$ID.$NAME.UPDATER_ERROR"
|
|
||||||
|
|
||||||
// Value containing action of BroadcastReceiver
|
|
||||||
internal const val EXTRA_ACTION = "$ID.$NAME.ACTION"
|
|
||||||
|
|
||||||
// Value containing progress
|
|
||||||
internal const val EXTRA_PROGRESS = "$ID.$NAME.PROGRESS"
|
|
||||||
|
|
||||||
// Value containing apk path
|
|
||||||
internal const val EXTRA_APK_PATH = "$ID.$NAME.APK_PATH"
|
|
||||||
|
|
||||||
// Value containing apk url
|
|
||||||
internal const val EXTRA_APK_URL = "$ID.$NAME.APK_URL"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification shown to user
|
|
||||||
*/
|
|
||||||
private val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
when (intent.getStringExtra(EXTRA_ACTION)) {
|
|
||||||
NOTIFICATION_UPDATER_INITIAL -> basicNotification()
|
|
||||||
NOTIFICATION_UPDATER_PROGRESS -> updateProgress(intent.getIntExtra(EXTRA_PROGRESS, 0))
|
|
||||||
NOTIFICATION_UPDATER_INSTALL -> installNotification(intent.getStringExtra(EXTRA_APK_PATH))
|
|
||||||
NOTIFICATION_UPDATER_ERROR -> errorNotification(intent.getStringExtra(EXTRA_APK_URL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to show basic notification
|
|
||||||
*/
|
|
||||||
private fun basicNotification() {
|
|
||||||
// Create notification
|
|
||||||
with(notification) {
|
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
|
||||||
setOngoing(true)
|
|
||||||
}
|
|
||||||
notification.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to show progress notification
|
|
||||||
*
|
|
||||||
* @param progress progress of download
|
|
||||||
*/
|
|
||||||
private fun updateProgress(progress: Int) {
|
|
||||||
with(notification) {
|
|
||||||
setProgress(100, progress, false)
|
|
||||||
setOnlyAlertOnce(true)
|
|
||||||
}
|
|
||||||
notification.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to show install notification
|
|
||||||
*
|
|
||||||
* @param path path of file
|
|
||||||
*/
|
|
||||||
private fun installNotification(path: String) {
|
|
||||||
// Prompt the user to install the new update.
|
|
||||||
with(notification) {
|
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
setOnlyAlertOnce(false)
|
|
||||||
setProgress(0, 0, false)
|
|
||||||
// Install action
|
|
||||||
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
|
|
||||||
addAction(R.drawable.ic_system_update_grey_24dp_img,
|
|
||||||
context.getString(R.string.action_install),
|
|
||||||
NotificationHandler.installApkPendingActivity(context, File(path)))
|
|
||||||
// Cancel action
|
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
|
||||||
context.getString(R.string.action_cancel),
|
|
||||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
|
|
||||||
}
|
|
||||||
notification.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to show error notification
|
|
||||||
*
|
|
||||||
* @param url url of apk
|
|
||||||
*/
|
|
||||||
private fun errorNotification(url: String) {
|
|
||||||
// Prompt the user to retry the download.
|
|
||||||
with(notification) {
|
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_error))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
|
||||||
setOnlyAlertOnce(false)
|
|
||||||
setProgress(0, 0, false)
|
|
||||||
// Retry action
|
|
||||||
addAction(R.drawable.ic_refresh_grey_24dp_img,
|
|
||||||
context.getString(R.string.action_retry),
|
|
||||||
UpdateDownloaderService.downloadApkPendingService(context, url))
|
|
||||||
// Cancel action
|
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
|
||||||
context.getString(R.string.action_cancel),
|
|
||||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
|
|
||||||
}
|
|
||||||
notification.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a notification from this builder.
|
|
||||||
*
|
|
||||||
* @param id the id of the notification.
|
|
||||||
*/
|
|
||||||
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
|
|
||||||
context.notificationManager.notify(id, build())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +1,67 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import com.evernote.android.job.Job
|
import com.evernote.android.job.Job
|
||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
import com.evernote.android.job.JobRequest
|
import com.evernote.android.job.JobRequest
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
|
||||||
class UpdateCheckerJob : Job() {
|
class UpdaterJob : Job() {
|
||||||
|
|
||||||
override fun onRunJob(params: Params): Result {
|
override fun onRunJob(params: Params): Result {
|
||||||
return GithubUpdateChecker()
|
return GithubUpdateChecker()
|
||||||
.checkForUpdate()
|
.checkForUpdate()
|
||||||
.map { result ->
|
.map { result ->
|
||||||
if (result is GithubUpdateResult.NewUpdate) {
|
if (result is GithubUpdateResult.NewUpdate) {
|
||||||
val url = result.release.downloadLink
|
val url = result.release.downloadLink
|
||||||
|
|
||||||
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||||
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
|
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
|
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
setContentText(context.getString(R.string.update_check_notification_update_available))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
// Download action
|
// Download action
|
||||||
addAction(android.R.drawable.stat_sys_download_done,
|
addAction(android.R.drawable.stat_sys_download_done,
|
||||||
context.getString(R.string.action_download),
|
context.getString(R.string.action_download),
|
||||||
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Job.Result.SUCCESS
|
Job.Result.SUCCESS
|
||||||
}
|
}
|
||||||
.onErrorReturn { Job.Result.FAILURE }
|
.onErrorReturn { Job.Result.FAILURE }
|
||||||
// Sadly, the task needs to be synchronous.
|
// Sadly, the task needs to be synchronous.
|
||||||
.toBlocking()
|
.toBlocking()
|
||||||
.single()
|
.single()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
|
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
|
||||||
block()
|
block()
|
||||||
context.notificationManager.notify(Notifications.ID_UPDATER, build())
|
context.notificationManager.notify(Notifications.ID_UPDATER, build())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "UpdateChecker"
|
const val TAG = "UpdateChecker"
|
||||||
|
|
||||||
fun setupTask() {
|
fun setupTask() {
|
||||||
JobRequest.Builder(TAG)
|
JobRequest.Builder(TAG)
|
||||||
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
|
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
|
||||||
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
|
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
|
||||||
.setRequirementsEnforced(true)
|
.setRequirementsEnforced(true)
|
||||||
.setUpdateCurrent(true)
|
.setUpdateCurrent(true)
|
||||||
.build()
|
.build()
|
||||||
.schedule()
|
.schedule()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelTask() {
|
fun cancelTask() {
|
||||||
JobManager.instance().cancelAllForTag(TAG)
|
JobManager.instance().cancelAllForTag(TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.support.v4.app.NotificationCompat
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DownloadNotifier is used to show notifications when downloading and update.
|
||||||
|
*
|
||||||
|
* @param context context of application.
|
||||||
|
*/
|
||||||
|
internal class UpdaterNotifier(private val context: Context) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder to manage notifications.
|
||||||
|
*/
|
||||||
|
private val notification by lazy {
|
||||||
|
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call to show notification.
|
||||||
|
*
|
||||||
|
* @param id id of the notification channel.
|
||||||
|
*/
|
||||||
|
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
|
||||||
|
context.notificationManager.notify(id, build())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call when apk download starts.
|
||||||
|
*
|
||||||
|
* @param title tile of notification.
|
||||||
|
*/
|
||||||
|
fun onDownloadStarted(title: String) {
|
||||||
|
with(notification) {
|
||||||
|
setContentTitle(title)
|
||||||
|
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
setOngoing(true)
|
||||||
|
}
|
||||||
|
notification.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call when apk download progress changes.
|
||||||
|
*
|
||||||
|
* @param progress progress of download (xx%/100).
|
||||||
|
*/
|
||||||
|
fun onProgressChange(progress: Int) {
|
||||||
|
with(notification) {
|
||||||
|
setProgress(100, progress, false)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
}
|
||||||
|
notification.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call when apk download is finished.
|
||||||
|
*
|
||||||
|
* @param uri path location of apk.
|
||||||
|
*/
|
||||||
|
fun onDownloadFinished(uri: Uri) {
|
||||||
|
with(notification) {
|
||||||
|
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
setOnlyAlertOnce(false)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
// Install action
|
||||||
|
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
|
||||||
|
addAction(R.drawable.ic_system_update_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_install),
|
||||||
|
NotificationHandler.installApkPendingActivity(context, uri))
|
||||||
|
// Cancel action
|
||||||
|
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_cancel),
|
||||||
|
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
|
||||||
|
}
|
||||||
|
notification.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call when apk download throws a error
|
||||||
|
*
|
||||||
|
* @param url web location of apk to download.
|
||||||
|
*/
|
||||||
|
fun onDownloadError(url: String) {
|
||||||
|
with(notification) {
|
||||||
|
setContentText(context.getString(R.string.update_check_notification_download_error))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
|
setOnlyAlertOnce(false)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
// Retry action
|
||||||
|
addAction(R.drawable.ic_refresh_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_retry),
|
||||||
|
UpdaterService.downloadApkPendingService(context, url))
|
||||||
|
// Cancel action
|
||||||
|
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_cancel),
|
||||||
|
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
|
||||||
|
}
|
||||||
|
notification.show(Notifications.ID_UPDATER)
|
||||||
|
}
|
||||||
|
}
|
@ -2,52 +2,37 @@ package eu.kanade.tachiyomi.data.updater
|
|||||||
|
|
||||||
import android.app.IntentService
|
import android.app.IntentService
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.ProgressListener
|
import eu.kanade.tachiyomi.network.ProgressListener
|
||||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||||
import eu.kanade.tachiyomi.util.registerLocalReceiver
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.saveTo
|
import eu.kanade.tachiyomi.util.saveTo
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcastSync
|
|
||||||
import eu.kanade.tachiyomi.util.unregisterLocalReceiver
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) {
|
class UpdaterService : IntentService(UpdaterService::class.java.name) {
|
||||||
/**
|
/**
|
||||||
* Network helper
|
* Network helper
|
||||||
*/
|
*/
|
||||||
private val network: NetworkHelper by injectLazy()
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local [BroadcastReceiver] that runs on UI thread
|
* Notifier for the updater state and progress.
|
||||||
*/
|
*/
|
||||||
private val updaterNotificationReceiver = UpdateDownloaderReceiver(this)
|
private val notifier by lazy { UpdaterNotifier(this) }
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
// Register receiver
|
|
||||||
registerLocalReceiver(updaterNotificationReceiver, IntentFilter(INTENT_FILTER_NAME))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
// Unregister receiver
|
|
||||||
unregisterLocalReceiver(updaterNotificationReceiver)
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onHandleIntent(intent: Intent?) {
|
override fun onHandleIntent(intent: Intent?) {
|
||||||
if (intent == null) return
|
if (intent == null) return
|
||||||
|
|
||||||
|
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
|
||||||
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
|
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
|
||||||
downloadApk(url)
|
downloadApk(title, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,9 +40,9 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
*
|
*
|
||||||
* @param url url location of file
|
* @param url url location of file
|
||||||
*/
|
*/
|
||||||
fun downloadApk(url: String) {
|
private fun downloadApk(title: String, url: String) {
|
||||||
// Show notification download starting.
|
// Show notification download starting.
|
||||||
sendInitialBroadcast()
|
notifier.onDownloadStarted(title)
|
||||||
|
|
||||||
val progressListener = object : ProgressListener {
|
val progressListener = object : ProgressListener {
|
||||||
|
|
||||||
@ -73,7 +58,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
if (progress > savedProgress && currentTime - 200 > lastTick) {
|
if (progress > savedProgress && currentTime - 200 > lastTick) {
|
||||||
savedProgress = progress
|
savedProgress = progress
|
||||||
lastTick = currentTime
|
lastTick = currentTime
|
||||||
sendProgressBroadcast(progress)
|
notifier.onProgressChange(progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,80 +76,32 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
response.close()
|
response.close()
|
||||||
throw Exception("Unsuccessful response")
|
throw Exception("Unsuccessful response")
|
||||||
}
|
}
|
||||||
sendInstallBroadcast(apkFile.absolutePath)
|
notifier.onDownloadFinished(apkFile.getUriCompat(this))
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
sendErrorBroadcast(url)
|
notifier.onDownloadError(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show notification download starting.
|
|
||||||
*/
|
|
||||||
private fun sendInitialBroadcast() {
|
|
||||||
val intent = Intent(INTENT_FILTER_NAME).apply {
|
|
||||||
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INITIAL)
|
|
||||||
}
|
|
||||||
sendLocalBroadcastSync(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show notification progress changed
|
|
||||||
*
|
|
||||||
* @param progress progress of download
|
|
||||||
*/
|
|
||||||
private fun sendProgressBroadcast(progress: Int) {
|
|
||||||
val intent = Intent(INTENT_FILTER_NAME).apply {
|
|
||||||
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS)
|
|
||||||
putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress)
|
|
||||||
}
|
|
||||||
sendLocalBroadcastSync(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show install notification.
|
|
||||||
*
|
|
||||||
* @param path location of file
|
|
||||||
*/
|
|
||||||
private fun sendInstallBroadcast(path: String){
|
|
||||||
val intent = Intent(INTENT_FILTER_NAME).apply {
|
|
||||||
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INSTALL)
|
|
||||||
putExtra(UpdateDownloaderReceiver.EXTRA_APK_PATH, path)
|
|
||||||
}
|
|
||||||
sendLocalBroadcastSync(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show error notification.
|
|
||||||
*
|
|
||||||
* @param url url of file
|
|
||||||
*/
|
|
||||||
private fun sendErrorBroadcast(url: String){
|
|
||||||
val intent = Intent(INTENT_FILTER_NAME).apply {
|
|
||||||
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_ERROR)
|
|
||||||
putExtra(UpdateDownloaderReceiver.EXTRA_APK_URL, url)
|
|
||||||
}
|
|
||||||
sendLocalBroadcastSync(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Name of Local BroadCastReceiver.
|
|
||||||
*/
|
|
||||||
private val INTENT_FILTER_NAME = UpdateDownloaderService::class.java.name
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download url.
|
* Download url.
|
||||||
*/
|
*/
|
||||||
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdateDownloaderService.DOWNLOAD_URL"
|
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download title
|
||||||
|
*/
|
||||||
|
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a new update and let the user install the new version from a notification.
|
* Downloads a new update and let the user install the new version from a notification.
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param url the url to the new update.
|
* @param url the url to the new update.
|
||||||
*/
|
*/
|
||||||
fun downloadUpdate(context: Context, url: String) {
|
fun downloadUpdate(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
|
||||||
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||||
|
putExtra(EXTRA_DOWNLOAD_TITLE, title)
|
||||||
putExtra(EXTRA_DOWNLOAD_URL, url)
|
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
@ -177,7 +114,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
|
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
|
||||||
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||||
putExtra(EXTRA_DOWNLOAD_URL, url)
|
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||||
}
|
}
|
||||||
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
@ -14,12 +14,14 @@ class CloudflareInterceptor : Interceptor {
|
|||||||
|
|
||||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||||
|
|
||||||
|
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val response = chain.proceed(chain.request())
|
val response = chain.proceed(chain.request())
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
// Check if Cloudflare anti-bot is on
|
||||||
if (response.code() == 503 && "cloudflare-nginx" == response.header("Server")) {
|
if (response.code() == 503 && serverCheck.contains(response.header("Server"))) {
|
||||||
return chain.proceed(resolveChallenge(response))
|
return chain.proceed(resolveChallenge(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,16 +18,6 @@ class NetworkHelper(context: Context) {
|
|||||||
.cache(Cache(cacheDir, cacheSize))
|
.cache(Cache(cacheDir, cacheSize))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val forceCacheClient = client.newBuilder()
|
|
||||||
.addNetworkInterceptor { chain ->
|
|
||||||
val originalResponse = chain.proceed(chain.request())
|
|
||||||
originalResponse.newBuilder()
|
|
||||||
.removeHeader("Pragma")
|
|
||||||
.header("Cache-Control", "max-age=600")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val cloudflareClient = client.newBuilder()
|
val cloudflareClient = client.newBuilder()
|
||||||
.addInterceptor(CloudflareInterceptor())
|
.addInterceptor(CloudflareInterceptor())
|
||||||
.build()
|
.build()
|
||||||
|
@ -17,7 +17,7 @@ class Mangafox : ParsedHttpSource() {
|
|||||||
|
|
||||||
override val name = "Mangafox"
|
override val name = "Mangafox"
|
||||||
|
|
||||||
override val baseUrl = "http://mangafox.me"
|
override val baseUrl = "http://mangafox.la"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class Mangahere : ParsedHttpSource() {
|
|||||||
|
|
||||||
override val name = "Mangahere"
|
override val name = "Mangahere"
|
||||||
|
|
||||||
override val baseUrl = "http://www.mangahere.co"
|
override val baseUrl = "http://www.mangahere.cc"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
@ -109,14 +109,21 @@ class Mangahere : ParsedHttpSource() {
|
|||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
val detailElement = document.select(".manga_detail_top").first()
|
val detailElement = document.select(".manga_detail_top").first()
|
||||||
val infoElement = detailElement.select(".detail_topText").first()
|
val infoElement = detailElement.select(".detail_topText").first()
|
||||||
|
val licensedElement = document.select(".mt10.color_ff00.mb10").first()
|
||||||
|
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").first()?.text()
|
manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").first()?.text()
|
||||||
manga.artist = infoElement.select("a[href^=//www.mangahere.co/artist/]").first()?.text()
|
manga.artist = infoElement.select("a[href^=//www.mangahere.co/artist/]").first()?.text()
|
||||||
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
|
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
|
||||||
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
|
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
|
||||||
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
|
|
||||||
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
|
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
|
||||||
|
|
||||||
|
if (licensedElement?.text()?.contains("licensed") == true) {
|
||||||
|
manga.status = SManga.LICENSED
|
||||||
|
} else {
|
||||||
|
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||||
|
}
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class Readmangatoday : ParsedHttpSource() {
|
|||||||
|
|
||||||
override val name = "ReadMangaToday"
|
override val name = "ReadMangaToday"
|
||||||
|
|
||||||
override val baseUrl = "http://www.readmng.com/"
|
override val baseUrl = "https://www.readmng.com"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
@ -96,14 +96,19 @@ class Readmangatoday : ParsedHttpSource() {
|
|||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
val detailElement = document.select("div.movie-meta").first()
|
val detailElement = document.select("div.movie-meta").first()
|
||||||
|
val genreElement = detailElement.select("dl.dl-horizontal > dd:eq(5) a")
|
||||||
|
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
manga.author = document.select("ul.cast-list li.director > ul a").first()?.text()
|
manga.author = document.select("ul.cast-list li.director > ul a").first()?.text()
|
||||||
manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text()
|
manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text()
|
||||||
manga.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text()
|
|
||||||
manga.description = detailElement.select("li.movie-detail").first()?.text()
|
manga.description = detailElement.select("li.movie-detail").first()?.text()
|
||||||
manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) }
|
manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||||
manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
|
manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
|
||||||
|
|
||||||
|
var genres = mutableListOf<String>()
|
||||||
|
genreElement?.forEach { genres.add(it.text()) }
|
||||||
|
manga.genre = genres.joinToString(", ")
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,13 +35,59 @@ class Mangachan : ParsedHttpSource() {
|
|||||||
val url = if (query.isNotEmpty()) {
|
val url = if (query.isNotEmpty()) {
|
||||||
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
|
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
|
||||||
} else {
|
} else {
|
||||||
val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
|
|
||||||
if (filt.isNotEmpty()) {
|
var genres = ""
|
||||||
var genres = ""
|
var order = ""
|
||||||
filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
|
var statusParam = true
|
||||||
"$baseUrl/tags/${genres.dropLast(1)}?offset=${20 * (pageNum - 1)}"
|
var status = ""
|
||||||
|
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||||
|
when (filter) {
|
||||||
|
is GenreList -> {
|
||||||
|
filter.state.forEach { f ->
|
||||||
|
if (!f.isIgnored()) {
|
||||||
|
genres += (if (f.isExcluded()) "-" else "") + f.id + '+'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OrderBy -> { if (filter.state!!.ascending && filter.state!!.index == 0) { statusParam = false } }
|
||||||
|
is Status -> status = arrayOf("", "all_done", "end", "ongoing", "new_ch")[filter.state]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (genres.isNotEmpty()) {
|
||||||
|
for (filter in filters) {
|
||||||
|
when (filter) {
|
||||||
|
is OrderBy -> {
|
||||||
|
order = if (filter.state!!.ascending) {
|
||||||
|
arrayOf("", "&n=favasc", "&n=abcdesc", "&n=chasc")[filter.state!!.index]
|
||||||
|
} else {
|
||||||
|
arrayOf("&n=dateasc", "&n=favdesc", "&n=abcasc", "&n=chdesc")[filter.state!!.index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusParam) {
|
||||||
|
"$baseUrl/tags/${genres.dropLast(1)}$order?offset=${20 * (pageNum - 1)}&status=$status"
|
||||||
|
} else {
|
||||||
|
"$baseUrl/tags/$status/${genres.dropLast(1)}/$order?offset=${20 * (pageNum - 1)}"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
|
for (filter in filters) {
|
||||||
|
when (filter) {
|
||||||
|
is OrderBy -> {
|
||||||
|
order = if (filter.state!!.ascending) {
|
||||||
|
arrayOf("manga/new", "manga/new&n=favasc", "manga/new&n=abcdesc", "manga/new&n=chasc")[filter.state!!.index]
|
||||||
|
} else {
|
||||||
|
arrayOf("manga/new&n=dateasc", "mostfavorites", "catalog", "sortch")[filter.state!!.index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusParam) {
|
||||||
|
"$baseUrl/$order?offset=${20 * (pageNum - 1)}&status=$status"
|
||||||
|
} else {
|
||||||
|
"$baseUrl/$order/$status?offset=${20 * (pageNum - 1)}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
@ -160,18 +206,39 @@ class Mangachan : ParsedHttpSource() {
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
||||||
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||||
|
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
|
||||||
|
private class OrderBy : Filter.Sort("Сортировка",
|
||||||
|
arrayOf("Дата", "Популярность", "Имя", "Главы"),
|
||||||
|
Filter.Sort.Selection(1, false))
|
||||||
|
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
Status(),
|
||||||
|
OrderBy(),
|
||||||
|
GenreList(getGenreList())
|
||||||
|
)
|
||||||
|
|
||||||
|
// private class StatusList(status: List<Status>) : Filter.Group<Status>("Статус", status)
|
||||||
|
// private class Status(name: String, val id: String) : Filter.CheckBox(name, false)
|
||||||
|
// private fun getStatusList() = listOf(
|
||||||
|
// Status("Перевод завершен", "/all_done"),
|
||||||
|
// Status("Выпуск завершен", "/end"),
|
||||||
|
// Status("Онгоинг", "/ongoing"),
|
||||||
|
// Status("Новые главы", "/new_ch")
|
||||||
|
// )
|
||||||
|
|
||||||
|
|
||||||
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
|
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
|
||||||
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
|
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
|
||||||
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
|
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
|
||||||
* on http://mangachan.me/
|
* on http://mangachan.me/
|
||||||
*/
|
*/
|
||||||
override fun getFilterList() = FilterList(
|
private fun getGenreList() = listOf(
|
||||||
Genre("18 плюс"),
|
Genre("18 плюс"),
|
||||||
Genre("bdsm"),
|
Genre("bdsm"),
|
||||||
Genre("арт"),
|
Genre("арт"),
|
||||||
Genre("биография"),
|
|
||||||
Genre("боевик"),
|
Genre("боевик"),
|
||||||
Genre("боевые искусства"),
|
Genre("боевые искусства"),
|
||||||
Genre("вампиры"),
|
Genre("вампиры"),
|
||||||
@ -191,7 +258,6 @@ class Mangachan : ParsedHttpSource() {
|
|||||||
Genre("кодомо"),
|
Genre("кодомо"),
|
||||||
Genre("комедия"),
|
Genre("комедия"),
|
||||||
Genre("литРПГ"),
|
Genre("литРПГ"),
|
||||||
Genre("магия"),
|
|
||||||
Genre("махо-сёдзё"),
|
Genre("махо-сёдзё"),
|
||||||
Genre("меха"),
|
Genre("меха"),
|
||||||
Genre("мистика"),
|
Genre("мистика"),
|
||||||
@ -213,6 +279,7 @@ class Mangachan : ParsedHttpSource() {
|
|||||||
Genre("сёдзё-ай"),
|
Genre("сёдзё-ай"),
|
||||||
Genre("сёнэн"),
|
Genre("сёнэн"),
|
||||||
Genre("сёнэн-ай"),
|
Genre("сёнэн-ай"),
|
||||||
|
Genre("темное фэнтези"),
|
||||||
Genre("тентакли"),
|
Genre("тентакли"),
|
||||||
Genre("трагедия"),
|
Genre("трагедия"),
|
||||||
Genre("триллер"),
|
Genre("триллер"),
|
||||||
@ -226,4 +293,4 @@ class Mangachan : ParsedHttpSource() {
|
|||||||
Genre("яой"),
|
Genre("яой"),
|
||||||
Genre("ёнкома")
|
Genre("ёнкома")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source.online.russian
|
|||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
@ -23,6 +24,11 @@ class Mintmanga : ParsedHttpSource() {
|
|||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun headersBuilder() = Headers.Builder().apply {
|
||||||
|
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||||
|
add("Referer", baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source.online.russian
|
|||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
@ -23,6 +24,11 @@ class Readmanga : ParsedHttpSource() {
|
|||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun headersBuilder() = Headers.Builder().apply {
|
||||||
|
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||||
|
add("Referer", baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.desc"
|
override fun popularMangaSelector() = "div.desc"
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "div.desc"
|
override fun latestUpdatesSelector() = "div.desc"
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.holder
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
import eu.davidea.flexibleadapter.items.ISectionable
|
||||||
|
import eu.kanade.tachiyomi.util.dpToPx
|
||||||
|
import io.github.mthli.slice.Slice
|
||||||
|
|
||||||
|
interface SlicedHolder {
|
||||||
|
|
||||||
|
val slice: Slice
|
||||||
|
|
||||||
|
val adapter: FlexibleAdapter<IFlexible<*>>
|
||||||
|
|
||||||
|
val viewToSlice: View
|
||||||
|
|
||||||
|
fun setCardEdges(item: ISectionable<*, *>) {
|
||||||
|
// Position of this item in its header. Defaults to 0 when header is null.
|
||||||
|
var position = 0
|
||||||
|
|
||||||
|
// Number of items in the header of this item. Defaults to 1 when header is null.
|
||||||
|
var count = 1
|
||||||
|
|
||||||
|
if (item.header != null) {
|
||||||
|
val sectionItems = adapter.getSectionItems(item.header)
|
||||||
|
position = sectionItems.indexOf(item)
|
||||||
|
count = sectionItems.size
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
// Only one item in the card
|
||||||
|
count == 1 -> applySlice(2f, false, false, true, true)
|
||||||
|
// First item of the card
|
||||||
|
position == 0 -> applySlice(2f, false, true, true, false)
|
||||||
|
// Last item of the card
|
||||||
|
position == count - 1 -> applySlice(2f, true, false, false, true)
|
||||||
|
// Middle item
|
||||||
|
else -> applySlice(0f, false, false, false, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean,
|
||||||
|
topShadow: Boolean, bottomShadow: Boolean) {
|
||||||
|
val margin = margin
|
||||||
|
|
||||||
|
slice.setRadius(radius)
|
||||||
|
slice.showLeftTopRect(topRect)
|
||||||
|
slice.showRightTopRect(topRect)
|
||||||
|
slice.showLeftBottomRect(bottomRect)
|
||||||
|
slice.showRightBottomRect(bottomRect)
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
slice.showTopEdgeShadow(topShadow)
|
||||||
|
slice.showBottomEdgeShadow(bottomShadow)
|
||||||
|
}
|
||||||
|
setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
if (viewToSlice.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||||
|
val p = viewToSlice.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
p.setMargins(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val margin
|
||||||
|
get() = 8.dpToPx
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.presenter
|
package eu.kanade.tachiyomi.ui.base.presenter
|
||||||
|
|
||||||
import nucleus.presenter.RxPresenter
|
import nucleus.presenter.RxPresenter
|
||||||
|
import nucleus.presenter.delivery.Delivery
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class BasePresenter<V> : RxPresenter<V>() {
|
open class BasePresenter<V> : RxPresenter<V>() {
|
||||||
@ -35,4 +36,29 @@ open class BasePresenter<V> : RxPresenter<V>() {
|
|||||||
fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
|
fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
|
||||||
= compose(deliverReplay<T>()).subscribe(split(onNext, onError)).apply { add(this) }
|
= compose(deliverReplay<T>()).subscribe(split(onNext, onError)).apply { add(this) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes an observable with [DeliverWithView] and adds it to the presenter's lifecycle
|
||||||
|
* subscription list.
|
||||||
|
*
|
||||||
|
* @param onNext function to execute when the observable emits an item.
|
||||||
|
* @param onError function to execute when the observable throws an error.
|
||||||
|
*/
|
||||||
|
fun <T> Observable<T>.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
|
||||||
|
= compose(DeliverWithView<V, T>(view())).subscribe(split(onNext, onError)).apply { add(this) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A deliverable that only emits to the view if attached, otherwise the event is ignored.
|
||||||
|
*/
|
||||||
|
class DeliverWithView<View, T>(private val view: Observable<View>) : Observable.Transformer<T, Delivery<View, T>> {
|
||||||
|
|
||||||
|
override fun call(observable: Observable<T>): Observable<Delivery<View, T>> {
|
||||||
|
return observable
|
||||||
|
.materialize()
|
||||||
|
.filter { notification -> !notification.isOnCompleted }
|
||||||
|
.flatMap { notification ->
|
||||||
|
view.take(1).filter { it != null }.map { Delivery(it, notification) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.catalogue
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
import android.support.v7.widget.SearchView
|
import android.support.v7.widget.SearchView
|
||||||
import android.view.*
|
import android.view.*
|
||||||
@ -15,6 +16,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
|
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||||
@ -46,7 +48,7 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
/**
|
/**
|
||||||
* Adapter containing sources.
|
* Adapter containing sources.
|
||||||
*/
|
*/
|
||||||
private var adapter : CatalogueAdapter? = null
|
private var adapter: CatalogueAdapter? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when controller is initialized.
|
* Called when controller is initialized.
|
||||||
@ -99,6 +101,8 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
recycler.layoutManager = LinearLayoutManager(view.context)
|
recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||||
|
|
||||||
|
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
@ -185,10 +189,11 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
// Create query listener which opens the global search view.
|
// Create query listener which opens the global search view.
|
||||||
searchView.queryTextChangeEvents()
|
searchView.queryTextChangeEvents()
|
||||||
.filter { it.isSubmitted }
|
.filter { it.isSubmitted }
|
||||||
.subscribeUntilDestroy {
|
.subscribeUntilDestroy { performGlobalSearch(it.queryText().toString()) }
|
||||||
val query = it.queryText().toString()
|
}
|
||||||
router.pushController(CatalogueSearchController(query).withFadeTransaction())
|
|
||||||
}
|
fun performGlobalSearch(query: String){
|
||||||
|
router.pushController(CatalogueSearchController(query).withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,23 +12,23 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
|
|||||||
private val divider: Drawable
|
private val divider: Drawable
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val a = context.obtainStyledAttributes(ATTRS)
|
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
|
||||||
divider = a.getDrawable(0)
|
divider = a.getDrawable(0)
|
||||||
a.recycle()
|
a.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
val left = parent.paddingLeft + SourceHolder.margin
|
|
||||||
val right = parent.width - parent.paddingRight - SourceHolder.margin
|
|
||||||
|
|
||||||
val childCount = parent.childCount
|
val childCount = parent.childCount
|
||||||
for (i in 0 until childCount - 1) {
|
for (i in 0 until childCount - 1) {
|
||||||
val child = parent.getChildAt(i)
|
val child = parent.getChildAt(i)
|
||||||
if (parent.getChildViewHolder(child) is SourceHolder &&
|
val holder = parent.getChildViewHolder(child)
|
||||||
|
if (holder is SourceHolder &&
|
||||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
|
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
|
||||||
val params = child.layoutParams as RecyclerView.LayoutParams
|
val params = child.layoutParams as RecyclerView.LayoutParams
|
||||||
val top = child.bottom + params.bottomMargin
|
val top = child.bottom + params.bottomMargin
|
||||||
val bottom = top + divider.intrinsicHeight
|
val bottom = top + divider.intrinsicHeight
|
||||||
|
val left = parent.paddingLeft + holder.margin
|
||||||
|
val right = parent.paddingRight + holder.margin
|
||||||
|
|
||||||
divider.setBounds(left, top, right, bottom)
|
divider.setBounds(left, top, right, bottom)
|
||||||
divider.draw(c)
|
divider.draw(c)
|
||||||
@ -41,7 +41,4 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
|
|||||||
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val ATTRS = intArrayOf(android.R.attr.listDivider)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,24 +1,27 @@
|
|||||||
package eu.kanade.tachiyomi.ui.catalogue
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.util.dpToPx
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
import eu.kanade.tachiyomi.util.getRound
|
import eu.kanade.tachiyomi.util.getRound
|
||||||
import eu.kanade.tachiyomi.util.gone
|
import eu.kanade.tachiyomi.util.gone
|
||||||
import eu.kanade.tachiyomi.util.visible
|
import eu.kanade.tachiyomi.util.visible
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
|
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
|
||||||
|
|
||||||
class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHolder(view, adapter) {
|
class SourceHolder(view: View, override val adapter: CatalogueAdapter) :
|
||||||
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
|
SlicedHolder {
|
||||||
|
|
||||||
private val slice = Slice(card).apply {
|
override val slice = Slice(card).apply {
|
||||||
setColor(adapter.cardBackground)
|
setColor(adapter.cardBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val viewToSlice: View
|
||||||
|
get() = card
|
||||||
|
|
||||||
init {
|
init {
|
||||||
source_browse.setOnClickListener {
|
source_browse.setOnClickListener {
|
||||||
adapter.browseClickListener.onBrowseClick(adapterPosition)
|
adapter.browseClickListener.onBrowseClick(adapterPosition)
|
||||||
@ -38,7 +41,7 @@ class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHold
|
|||||||
|
|
||||||
// Set circle letter image.
|
// Set circle letter image.
|
||||||
itemView.post {
|
itemView.post {
|
||||||
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
|
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(), false))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If source is login, show only login option
|
// If source is login, show only login option
|
||||||
@ -47,59 +50,11 @@ class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHold
|
|||||||
source_latest.gone()
|
source_latest.gone()
|
||||||
} else {
|
} else {
|
||||||
source_browse.setText(R.string.browse)
|
source_browse.setText(R.string.browse)
|
||||||
source_latest.visible()
|
if (source.supportsLatest) {
|
||||||
|
source_latest.visible()
|
||||||
|
} else {
|
||||||
|
source_latest.gone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setCardEdges(item: SourceItem) {
|
|
||||||
// Position of this item in its header. Defaults to 0 when header is null.
|
|
||||||
var position = 0
|
|
||||||
|
|
||||||
// Number of items in the header of this item. Defaults to 1 when header is null.
|
|
||||||
var count = 1
|
|
||||||
|
|
||||||
if (item.header != null) {
|
|
||||||
val sectionItems = mAdapter.getSectionItems(item.header)
|
|
||||||
position = sectionItems.indexOf(item)
|
|
||||||
count = sectionItems.size
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
|
||||||
// Only one item in the card
|
|
||||||
count == 1 -> applySlice(2f, false, false, true, true)
|
|
||||||
// First item of the card
|
|
||||||
position == 0 -> applySlice(2f, false, true, true, false)
|
|
||||||
// Last item of the card
|
|
||||||
position == count - 1 -> applySlice(2f, true, false, false, true)
|
|
||||||
// Middle item
|
|
||||||
else -> applySlice(0f, false, false, false, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean,
|
|
||||||
topShadow: Boolean, bottomShadow: Boolean) {
|
|
||||||
|
|
||||||
slice.setRadius(radius)
|
|
||||||
slice.showLeftTopRect(topRect)
|
|
||||||
slice.showRightTopRect(topRect)
|
|
||||||
slice.showLeftBottomRect(bottomRect)
|
|
||||||
slice.showRightBottomRect(bottomRect)
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
slice.showTopEdgeShadow(topShadow)
|
|
||||||
slice.showBottomEdgeShadow(bottomShadow)
|
|
||||||
}
|
|
||||||
setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
|
|
||||||
val v = card
|
|
||||||
if (v.layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
val p = v.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
p.setMargins(left, top, right, bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val margin = 8.dpToPx
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -24,7 +24,6 @@ import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
|||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.*
|
import eu.kanade.tachiyomi.util.*
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
|
|
||||||
import kotlinx.android.synthetic.main.catalogue_controller.*
|
import kotlinx.android.synthetic.main.catalogue_controller.*
|
||||||
import kotlinx.android.synthetic.main.main_activity.*
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -75,11 +74,6 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
*/
|
*/
|
||||||
private var recycler: RecyclerView? = null
|
private var recycler: RecyclerView? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Drawer listener to allow swipe only for closing the drawer.
|
|
||||||
*/
|
|
||||||
private var drawerListener: DrawerLayout.DrawerListener? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription for the search view.
|
* Subscription for the search view.
|
||||||
*/
|
*/
|
||||||
@ -138,17 +132,15 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
// Inflate and prepare drawer
|
// Inflate and prepare drawer
|
||||||
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
|
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
|
||||||
this.navView = navView
|
this.navView = navView
|
||||||
drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
|
|
||||||
drawer.addDrawerListener(it)
|
|
||||||
}
|
|
||||||
navView.setFilters(presenter.filterItems)
|
navView.setFilters(presenter.filterItems)
|
||||||
|
|
||||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
|
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
|
||||||
|
|
||||||
navView.onSearchClicked = {
|
navView.onSearchClicked = {
|
||||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||||
showProgressBar()
|
showProgressBar()
|
||||||
adapter?.clear()
|
adapter?.clear()
|
||||||
|
drawer.closeDrawer(Gravity.END)
|
||||||
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
|
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,8 +154,6 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
|
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
|
||||||
drawerListener?.let { drawer.removeDrawerListener(it) }
|
|
||||||
drawerListener = null
|
|
||||||
navView = null
|
navView = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,13 +161,13 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
numColumnsSubscription?.unsubscribe()
|
numColumnsSubscription?.unsubscribe()
|
||||||
|
|
||||||
var oldPosition = RecyclerView.NO_POSITION
|
var oldPosition = RecyclerView.NO_POSITION
|
||||||
val oldRecycler = catalogue_view?.getChildAt(1)
|
val oldRecycler = catalogue_view?.getChildAt(1)
|
||||||
if (oldRecycler is RecyclerView) {
|
if (oldRecycler is RecyclerView) {
|
||||||
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||||
oldRecycler.adapter = null
|
oldRecycler.adapter = null
|
||||||
|
|
||||||
catalogue_view?.removeView(oldRecycler)
|
catalogue_view?.removeView(oldRecycler)
|
||||||
}
|
}
|
||||||
|
|
||||||
val recycler = if (presenter.isListMode) {
|
val recycler = if (presenter.isListMode) {
|
||||||
RecyclerView(view.context).apply {
|
RecyclerView(view.context).apply {
|
||||||
@ -476,6 +466,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
0 -> {
|
0 -> {
|
||||||
presenter.changeMangaFavorite(manga)
|
presenter.changeMangaFavorite(manga)
|
||||||
adapter?.notifyItemChanged(position)
|
adapter?.notifyItemChanged(position)
|
||||||
|
activity?.toast(activity?.getString(R.string.manga_removed_library))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
@ -498,6 +489,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||||
.showDialog(router)
|
.showDialog(router)
|
||||||
}
|
}
|
||||||
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
|
|||||||
val view = inflate(R.layout.catalogue_drawer_content)
|
val view = inflate(R.layout.catalogue_drawer_content)
|
||||||
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
|
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
|
||||||
addView(view)
|
addView(view)
|
||||||
|
title.text = context?.getString(R.string.source_search_options)
|
||||||
search_btn.setOnClickListener { onSearchClicked() }
|
search_btn.setOnClickListener { onSearchClicked() }
|
||||||
reset_btn.setOnClickListener { onResetClicked() }
|
reset_btn.setOnClickListener { onResetClicked() }
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,10 @@ import eu.kanade.tachiyomi.util.setVectorCompat
|
|||||||
|
|
||||||
class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {
|
class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
isExpanded = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
override fun getLayoutRes(): Int {
|
||||||
return R.layout.navigation_view_group
|
return R.layout.navigation_view_group
|
||||||
}
|
}
|
||||||
@ -32,6 +36,9 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
|
|||||||
R.drawable.ic_expand_more_white_24dp
|
R.drawable.ic_expand_more_white_24dp
|
||||||
else
|
else
|
||||||
R.drawable.ic_chevron_right_white_24dp)
|
R.drawable.ic_chevron_right_white_24dp)
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener(holder)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@ -44,6 +51,7 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
|
|||||||
return filter.hashCode()
|
return filter.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
|
open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
|
||||||
|
|
||||||
val title: TextView = itemView.findViewById(R.id.title)
|
val title: TextView = itemView.findViewById(R.id.title)
|
||||||
@ -52,5 +60,6 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
|
|||||||
override fun shouldNotifyParentOnClick(): Boolean {
|
override fun shouldNotifyParentOnClick(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,6 +10,10 @@ import eu.kanade.tachiyomi.util.setVectorCompat
|
|||||||
|
|
||||||
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
|
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
isExpanded = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
override fun getLayoutRes(): Int {
|
||||||
return R.layout.navigation_view_group
|
return R.layout.navigation_view_group
|
||||||
}
|
}
|
||||||
@ -29,6 +33,9 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGrou
|
|||||||
R.drawable.ic_expand_more_white_24dp
|
R.drawable.ic_expand_more_white_24dp
|
||||||
else
|
else
|
||||||
R.drawable.ic_chevron_right_white_24dp)
|
R.drawable.ic_chevron_right_white_24dp)
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener(holder)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
@ -33,9 +33,9 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
|
|||||||
val i = filter.values.indexOf(name)
|
val i = filter.values.indexOf(name)
|
||||||
|
|
||||||
fun getIcon() = when (filter.state) {
|
fun getIcon() = when (filter.state) {
|
||||||
Filter.Sort.Selection(i, false) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_down_black_32dp, null)
|
Filter.Sort.Selection(i, false) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_arrow_down_32dp, null)
|
||||||
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
|
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
|
||||||
Filter.Sort.Selection(i, true) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_up_black_32dp, null)
|
Filter.Sort.Selection(i, true) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_arrow_up_32dp, null)
|
||||||
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
|
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
|
||||||
else -> ContextCompat.getDrawable(view.context, R.drawable.empty_drawable_32dp)
|
else -> ContextCompat.getDrawable(view.context, R.drawable.empty_drawable_32dp)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ class CatalogueSearchCardAdapter(controller: CatalogueSearchController) :
|
|||||||
*/
|
*/
|
||||||
interface OnMangaClickListener {
|
interface OnMangaClickListener {
|
||||||
fun onMangaClick(manga: Manga)
|
fun onMangaClick(manga: Manga)
|
||||||
|
fun onMangaLongClick(manga: Manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -19,10 +19,19 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
|
|||||||
adapter.mangaClickListener.onMangaClick(item.manga)
|
adapter.mangaClickListener.onMangaClick(item.manga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
itemView.setOnLongClickListener {
|
||||||
|
val item = adapter.getItem(adapterPosition)
|
||||||
|
if (item != null) {
|
||||||
|
adapter.mangaClickListener.onMangaLongClick(item.manga)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(manga: Manga) {
|
fun bind(manga: Manga) {
|
||||||
tvTitle.text = manga.title
|
tvTitle.text = manga.title
|
||||||
|
// Set alpha of thumbnail.
|
||||||
|
itemImage.alpha = if (manga.favorite) 0.3f else 1.0f
|
||||||
|
|
||||||
setImage(manga)
|
setImage(manga)
|
||||||
}
|
}
|
||||||
|
@ -18,14 +18,14 @@ import kotlinx.android.synthetic.main.catalogue_global_search_controller.*
|
|||||||
* This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter]
|
* This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter]
|
||||||
* [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
* [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||||
*/
|
*/
|
||||||
class CatalogueSearchController(private val initialQuery: String? = null) :
|
open class CatalogueSearchController(protected val initialQuery: String? = null) :
|
||||||
NucleusController<CatalogueSearchPresenter>(),
|
NucleusController<CatalogueSearchPresenter>(),
|
||||||
CatalogueSearchCardAdapter.OnMangaClickListener {
|
CatalogueSearchCardAdapter.OnMangaClickListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter containing search results grouped by lang.
|
* Adapter containing search results grouped by lang.
|
||||||
*/
|
*/
|
||||||
private var adapter: CatalogueSearchAdapter? = null
|
protected var adapter: CatalogueSearchAdapter? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when controller is initialized.
|
* Called when controller is initialized.
|
||||||
@ -73,6 +73,16 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
|
|||||||
router.pushController(MangaController(manga, true).withFadeTransaction())
|
router.pushController(MangaController(manga, true).withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when manga in global search is long clicked.
|
||||||
|
*
|
||||||
|
* @param manga clicked item containing manga information.
|
||||||
|
*/
|
||||||
|
override fun onMangaLongClick(manga: Manga) {
|
||||||
|
// Delegate to single click by default.
|
||||||
|
onMangaClick(manga)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds items to the options menu.
|
* Adds items to the options menu.
|
||||||
*
|
*
|
||||||
|
@ -30,7 +30,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
* @param db manages the database calls.
|
* @param db manages the database calls.
|
||||||
* @param preferencesHelper manages the preference calls.
|
* @param preferencesHelper manages the preference calls.
|
||||||
*/
|
*/
|
||||||
class CatalogueSearchPresenter(
|
open class CatalogueSearchPresenter(
|
||||||
val initialQuery: String? = "",
|
val initialQuery: String? = "",
|
||||||
val sourceManager: SourceManager = Injekt.get(),
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
val db: DatabaseHelper = Injekt.get(),
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
@ -86,7 +86,7 @@ class CatalogueSearchPresenter(
|
|||||||
*
|
*
|
||||||
* @return list containing enabled sources.
|
* @return list containing enabled sources.
|
||||||
*/
|
*/
|
||||||
private fun getEnabledSources(): List<CatalogueSource> {
|
protected open fun getEnabledSources(): List<CatalogueSource> {
|
||||||
val languages = preferencesHelper.enabledLanguages().getOrDefault()
|
val languages = preferencesHelper.enabledLanguages().getOrDefault()
|
||||||
val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault()
|
val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault()
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
|||||||
CategoryAdapter.OnItemReleaseListener,
|
CategoryAdapter.OnItemReleaseListener,
|
||||||
CategoryCreateDialog.Listener,
|
CategoryCreateDialog.Listener,
|
||||||
CategoryRenameDialog.Listener,
|
CategoryRenameDialog.Listener,
|
||||||
UndoHelper.OnUndoListener {
|
UndoHelper.OnActionListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object used to show ActionMode toolbar.
|
* Object used to show ActionMode toolbar.
|
||||||
@ -107,9 +107,14 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
|||||||
fun setCategories(categories: List<CategoryItem>) {
|
fun setCategories(categories: List<CategoryItem>) {
|
||||||
actionMode?.finish()
|
actionMode?.finish()
|
||||||
adapter?.updateDataSet(categories)
|
adapter?.updateDataSet(categories)
|
||||||
val selected = categories.filter { it.isSelected }
|
if (categories.isNotEmpty()) {
|
||||||
if (selected.isNotEmpty()) {
|
empty_view.hide()
|
||||||
selected.forEach { onItemLongClick(categories.indexOf(it)) }
|
val selected = categories.filter { it.isSelected }
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
selected.forEach { onItemLongClick(categories.indexOf(it)) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
empty_view.show(R.drawable.ic_shape_black_128dp, R.string.information_empty_category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +168,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
|||||||
R.id.action_delete -> {
|
R.id.action_delete -> {
|
||||||
undoHelper = UndoHelper(adapter, this)
|
undoHelper = UndoHelper(adapter, this)
|
||||||
undoHelper?.start(adapter.selectedPositions, view!!,
|
undoHelper?.start(adapter.selectedPositions, view!!,
|
||||||
R.string.snack_categories_deleted, R.string.action_undo, 3000)
|
R.string.snack_categories_deleted, R.string.action_undo, 3000)
|
||||||
|
|
||||||
mode.finish()
|
mode.finish()
|
||||||
}
|
}
|
||||||
@ -263,7 +268,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
|||||||
*
|
*
|
||||||
* @param action The action performed.
|
* @param action The action performed.
|
||||||
*/
|
*/
|
||||||
override fun onActionCanceled(action: Int) {
|
override fun onActionCanceled(action: Int, positions: MutableList<Int>?) {
|
||||||
adapter?.restoreDeletedItems()
|
adapter?.restoreDeletedItems()
|
||||||
undoHelper = null
|
undoHelper = null
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RouterPagerAdapter
|
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
||||||
@ -34,6 +32,7 @@ import kotlinx.android.synthetic.main.manga_controller.*
|
|||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class MangaController : RxController, TabbedController {
|
class MangaController : RxController, TabbedController {
|
||||||
|
|
||||||
@ -63,6 +62,8 @@ class MangaController : RxController, TabbedController {
|
|||||||
|
|
||||||
val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
|
val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
|
||||||
|
|
||||||
|
val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
|
||||||
|
|
||||||
val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
|
val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
|
||||||
|
|
||||||
val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
|
val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
|
||||||
@ -188,4 +189,5 @@ class MangaController : RxController, TabbedController {
|
|||||||
.apply { isAccessible = true }
|
.apply { isAccessible = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ class ChapterHolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the correct drawable for dropdown and update the tint to match theme.
|
// Set the correct drawable for dropdown and update the tint to match theme.
|
||||||
chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
|
chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color))
|
||||||
|
|
||||||
// Set correct text color
|
// Set correct text color
|
||||||
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
|
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.manga.chapter
|
|||||||
|
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.support.design.widget.Snackbar
|
import android.support.design.widget.Snackbar
|
||||||
@ -36,6 +37,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
SetDisplayModeDialog.Listener,
|
SetDisplayModeDialog.Listener,
|
||||||
SetSortingDialog.Listener,
|
SetSortingDialog.Listener,
|
||||||
DownloadChaptersDialog.Listener,
|
DownloadChaptersDialog.Listener,
|
||||||
|
DownloadCustomChaptersDialog.Listener,
|
||||||
DeleteChaptersDialog.Listener {
|
DeleteChaptersDialog.Listener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,7 +63,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
override fun createPresenter(): ChaptersPresenter {
|
override fun createPresenter(): ChaptersPresenter {
|
||||||
val ctrl = parentController as MangaController
|
val ctrl = parentController as MangaController
|
||||||
return ChaptersPresenter(ctrl.manga!!, ctrl.source!!,
|
return ChaptersPresenter(ctrl.manga!!, ctrl.source!!,
|
||||||
ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay)
|
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
@ -209,7 +211,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchChaptersFromSource() {
|
private fun fetchChaptersFromSource() {
|
||||||
swipe_refresh?.isRefreshing = true
|
swipe_refresh?.isRefreshing = true
|
||||||
presenter.fetchChaptersFromSource()
|
presenter.fetchChaptersFromSource()
|
||||||
}
|
}
|
||||||
@ -271,18 +273,18 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
actionMode?.invalidate()
|
actionMode?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSelectedChapters(): List<ChapterItem> {
|
private fun getSelectedChapters(): List<ChapterItem> {
|
||||||
val adapter = adapter ?: return emptyList()
|
val adapter = adapter ?: return emptyList()
|
||||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
|
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createActionModeIfNeeded() {
|
private fun createActionModeIfNeeded() {
|
||||||
if (actionMode == null) {
|
if (actionMode == null) {
|
||||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroyActionModeIfNeeded() {
|
private fun destroyActionModeIfNeeded() {
|
||||||
actionMode?.finish()
|
actionMode?.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,6 +294,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StringFormatInvalid")
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
val count = adapter?.selectedItemCount ?: 0
|
val count = adapter?.selectedItemCount ?: 0
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
@ -339,25 +342,25 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
|
|
||||||
// SELECTION MODE ACTIONS
|
// SELECTION MODE ACTIONS
|
||||||
|
|
||||||
fun selectAll() {
|
private fun selectAll() {
|
||||||
val adapter = adapter ?: return
|
val adapter = adapter ?: return
|
||||||
adapter.selectAll()
|
adapter.selectAll()
|
||||||
selectedItems.addAll(adapter.items)
|
selectedItems.addAll(adapter.items)
|
||||||
actionMode?.invalidate()
|
actionMode?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAsRead(chapters: List<ChapterItem>) {
|
private fun markAsRead(chapters: List<ChapterItem>) {
|
||||||
presenter.markChaptersRead(chapters, true)
|
presenter.markChaptersRead(chapters, true)
|
||||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
||||||
deleteChapters(chapters)
|
deleteChapters(chapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAsUnread(chapters: List<ChapterItem>) {
|
private fun markAsUnread(chapters: List<ChapterItem>) {
|
||||||
presenter.markChaptersRead(chapters, false)
|
presenter.markChaptersRead(chapters, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadChapters(chapters: List<ChapterItem>) {
|
private fun downloadChapters(chapters: List<ChapterItem>) {
|
||||||
val view = view
|
val view = view
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
presenter.downloadChapters(chapters)
|
presenter.downloadChapters(chapters)
|
||||||
@ -370,6 +373,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun showDeleteChaptersConfirmationDialog() {
|
private fun showDeleteChaptersConfirmationDialog() {
|
||||||
DeleteChaptersDialog(this).showDialog(router)
|
DeleteChaptersDialog(this).showDialog(router)
|
||||||
}
|
}
|
||||||
@ -378,7 +382,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
deleteChapters(getSelectedChapters())
|
deleteChapters(getSelectedChapters())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markPreviousAsRead(chapter: ChapterItem) {
|
private fun markPreviousAsRead(chapter: ChapterItem) {
|
||||||
val adapter = adapter ?: return
|
val adapter = adapter ?: return
|
||||||
val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
||||||
val chapterPos = chapters.indexOf(chapter)
|
val chapterPos = chapters.indexOf(chapter)
|
||||||
@ -387,7 +391,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
|
private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
presenter.bookmarkChapters(chapters, bookmarked)
|
presenter.bookmarkChapters(chapters, bookmarked)
|
||||||
}
|
}
|
||||||
@ -410,7 +414,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissDeletingDialog() {
|
private fun dismissDeletingDialog() {
|
||||||
router.popControllerWithTag(DeletingChaptersDialog.TAG)
|
router.popControllerWithTag(DeletingChaptersDialog.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,29 +443,44 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
DownloadChaptersDialog(this).showDialog(router)
|
DownloadChaptersDialog(this).showDialog(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun downloadChapters(choice: Int) {
|
private fun getUnreadChaptersSorted() = presenter.chapters
|
||||||
fun getUnreadChaptersSorted() = presenter.chapters
|
.filter { !it.read && it.status == Download.NOT_DOWNLOADED }
|
||||||
.filter { !it.read && it.status == Download.NOT_DOWNLOADED }
|
.distinctBy { it.name }
|
||||||
.distinctBy { it.name }
|
.sortedByDescending { it.source_order }
|
||||||
.sortedByDescending { it.source_order }
|
|
||||||
|
|
||||||
// i = 0: Download 1
|
|
||||||
// i = 1: Download 5
|
|
||||||
// i = 2: Download 10
|
|
||||||
// i = 3: Download unread
|
|
||||||
// i = 4: Download all
|
|
||||||
val chaptersToDownload = when (choice) {
|
|
||||||
0 -> getUnreadChaptersSorted().take(1)
|
|
||||||
1 -> getUnreadChaptersSorted().take(5)
|
|
||||||
2 -> getUnreadChaptersSorted().take(10)
|
|
||||||
3 -> presenter.chapters.filter { !it.read }
|
|
||||||
4 -> presenter.chapters
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override fun downloadCustomChapters(amount: Int) {
|
||||||
|
val chaptersToDownload = getUnreadChaptersSorted().take(amount)
|
||||||
if (chaptersToDownload.isNotEmpty()) {
|
if (chaptersToDownload.isNotEmpty()) {
|
||||||
downloadChapters(chaptersToDownload)
|
downloadChapters(chaptersToDownload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showCustomDownloadDialog() {
|
||||||
|
DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun downloadChapters(choice: Int) {
|
||||||
|
// i = 0: Download 1
|
||||||
|
// i = 1: Download 5
|
||||||
|
// i = 2: Download 10
|
||||||
|
// i = 3: Download x
|
||||||
|
// i = 4: Download unread
|
||||||
|
// i = 5: Download all
|
||||||
|
val chaptersToDownload = when (choice) {
|
||||||
|
0 -> getUnreadChaptersSorted().take(1)
|
||||||
|
1 -> getUnreadChaptersSorted().take(5)
|
||||||
|
2 -> getUnreadChaptersSorted().take(10)
|
||||||
|
3 -> {
|
||||||
|
showCustomDownloadDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
4 -> presenter.chapters.filter { !it.read }
|
||||||
|
5 -> presenter.chapters
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
if (chaptersToDownload.isNotEmpty()) {
|
||||||
|
downloadChapters(chaptersToDownload)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import rx.schedulers.Schedulers
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [ChaptersController].
|
* Presenter of [ChaptersController].
|
||||||
@ -28,6 +29,7 @@ class ChaptersPresenter(
|
|||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val source: Source,
|
val source: Source,
|
||||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
private val chapterCountRelay: BehaviorRelay<Float>,
|
||||||
|
private val lastUpdateRelay: BehaviorRelay<Date>,
|
||||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
@ -91,6 +93,11 @@ class ChaptersPresenter(
|
|||||||
// Emit the number of chapters to the info tab.
|
// Emit the number of chapters to the info tab.
|
||||||
chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number
|
chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number
|
||||||
?: 0f)
|
?: 0f)
|
||||||
|
|
||||||
|
// Emit the upload date of the most recent chapter
|
||||||
|
lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload
|
||||||
|
?: 0))
|
||||||
|
|
||||||
}
|
}
|
||||||
.subscribe { chaptersRelay.call(it) })
|
.subscribe { chaptersRelay.call(it) })
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,12 @@ class DownloadChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundl
|
|||||||
R.string.download_1,
|
R.string.download_1,
|
||||||
R.string.download_5,
|
R.string.download_5,
|
||||||
R.string.download_10,
|
R.string.download_10,
|
||||||
|
R.string.download_custom,
|
||||||
R.string.download_unread,
|
R.string.download_unread,
|
||||||
R.string.download_all
|
R.string.download_all
|
||||||
).map { activity.getString(it) }
|
).map { activity.getString(it) }
|
||||||
|
|
||||||
return MaterialDialog.Builder(activity)
|
return MaterialDialog.Builder(activity)
|
||||||
.title(R.string.manga_download)
|
|
||||||
.negativeText(android.R.string.cancel)
|
.negativeText(android.R.string.cancel)
|
||||||
.items(choices)
|
.items(choices)
|
||||||
.itemsCallback { _, _, position, _ ->
|
.itemsCallback { _, _, position, _ ->
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.widget.DialogCustomDownloadView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog used to let user select amount of chapters to download.
|
||||||
|
*/
|
||||||
|
class DownloadCustomChaptersDialog<T> : DialogController
|
||||||
|
where T : Controller, T : DownloadCustomChaptersDialog.Listener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of chapters to download in download chooser.
|
||||||
|
*/
|
||||||
|
private val maxChapters: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize dialog.
|
||||||
|
* @param maxChapters maximal number of chapters that user can download.
|
||||||
|
*/
|
||||||
|
constructor(target: T, maxChapters: Int) : super(Bundle().apply {
|
||||||
|
// Add maximum number of chapters to download value to bundle.
|
||||||
|
putInt(KEY_ITEM_MAX, maxChapters)
|
||||||
|
}) {
|
||||||
|
targetController = target
|
||||||
|
this.maxChapters = maxChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore dialog.
|
||||||
|
* @param bundle bundle containing data from state restore.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
constructor(bundle: Bundle) : super(bundle) {
|
||||||
|
// Get maximum chapters to download from bundle
|
||||||
|
val maxChapters = bundle.getInt(KEY_ITEM_MAX, 0)
|
||||||
|
this.maxChapters = maxChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when dialog is being created.
|
||||||
|
*/
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
val activity = activity!!
|
||||||
|
|
||||||
|
// Initialize view that lets user select number of chapters to download.
|
||||||
|
val view = DialogCustomDownloadView(activity).apply {
|
||||||
|
setMinMax(0, maxChapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build dialog.
|
||||||
|
// when positive dialog is pressed call custom listener.
|
||||||
|
return MaterialDialog.Builder(activity)
|
||||||
|
.title(R.string.custom_download)
|
||||||
|
.customView(view, true)
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onPositive { _, _ ->
|
||||||
|
(targetController as? Listener)?.downloadCustomChapters(view.amount)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun downloadCustomChapters(amount: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
// Key to retrieve max chapters from bundle on process death.
|
||||||
|
const val KEY_ITEM_MAX = "DownloadCustomChaptersDialog.int.maxChapters"
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.manga.info
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
@ -13,6 +16,7 @@ import android.support.v4.content.pm.ShortcutInfoCompat
|
|||||||
import android.support.v4.content.pm.ShortcutManagerCompat
|
import android.support.v4.content.pm.ShortcutManagerCompat
|
||||||
import android.support.v4.graphics.drawable.IconCompat
|
import android.support.v4.graphics.drawable.IconCompat
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import android.widget.Toast
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
@ -20,6 +24,7 @@ import com.bumptech.glide.request.target.SimpleTarget
|
|||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||||
import com.jakewharton.rxbinding.view.clicks
|
import com.jakewharton.rxbinding.view.clicks
|
||||||
|
import com.jakewharton.rxbinding.view.longClicks
|
||||||
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
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -31,17 +36,22 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.getResourceColor
|
import eu.kanade.tachiyomi.util.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.snack
|
import eu.kanade.tachiyomi.util.snack
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
|
import eu.kanade.tachiyomi.util.truncateCenter
|
||||||
import jp.wasabeef.glide.transformations.CropSquareTransformation
|
import jp.wasabeef.glide.transformations.CropSquareTransformation
|
||||||
import jp.wasabeef.glide.transformations.MaskTransformation
|
import jp.wasabeef.glide.transformations.MaskTransformation
|
||||||
import kotlinx.android.synthetic.main.manga_info_controller.*
|
import kotlinx.android.synthetic.main.manga_info_controller.*
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.DateFormat
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment that shows manga information.
|
* Fragment that shows manga information.
|
||||||
@ -64,7 +74,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
override fun createPresenter(): MangaInfoPresenter {
|
override fun createPresenter(): MangaInfoPresenter {
|
||||||
val ctrl = parentController as MangaController
|
val ctrl = parentController as MangaController
|
||||||
return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!,
|
return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!,
|
||||||
ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay)
|
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
@ -79,6 +89,41 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
|
|
||||||
// Set SwipeRefresh to refresh manga data.
|
// Set SwipeRefresh to refresh manga data.
|
||||||
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
|
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
|
||||||
|
|
||||||
|
manga_full_title.longClicks().subscribeUntilDestroy {
|
||||||
|
copyToClipboard(view.context.getString(R.string.title), manga_full_title.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_full_title.clicks().subscribeUntilDestroy {
|
||||||
|
performGlobalSearch(manga_full_title.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_artist.longClicks().subscribeUntilDestroy {
|
||||||
|
copyToClipboard(manga_artist_label.text.toString(), manga_artist.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_artist.clicks().subscribeUntilDestroy {
|
||||||
|
performGlobalSearch(manga_artist.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_author.longClicks().subscribeUntilDestroy {
|
||||||
|
copyToClipboard(manga_author.text.toString(), manga_author.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_author.clicks().subscribeUntilDestroy {
|
||||||
|
performGlobalSearch(manga_author.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_summary.longClicks().subscribeUntilDestroy {
|
||||||
|
copyToClipboard(view.context.getString(R.string.description), manga_summary.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_genres_tags.setOnTagClickListener { tag -> performGlobalSearch(tag) }
|
||||||
|
|
||||||
|
manga_cover.longClicks().subscribeUntilDestroy {
|
||||||
|
copyToClipboard(view.context.getString(R.string.title), presenter.manga.title)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
@ -107,6 +152,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
if (manga.initialized) {
|
if (manga.initialized) {
|
||||||
// Update view.
|
// Update view.
|
||||||
setMangaInfo(manga, source)
|
setMangaInfo(manga, source)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Initialize manga.
|
// Initialize manga.
|
||||||
fetchMangaFromSource()
|
fetchMangaFromSource()
|
||||||
@ -122,19 +168,45 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
private fun setMangaInfo(manga: Manga, source: Source?) {
|
private fun setMangaInfo(manga: Manga, source: Source?) {
|
||||||
val view = view ?: return
|
val view = view ?: return
|
||||||
|
|
||||||
// Update artist TextView.
|
//update full title TextView.
|
||||||
manga_artist.text = manga.artist
|
manga_full_title.text = if (manga.title.isBlank()) {
|
||||||
|
view.context.getString(R.string.unknown)
|
||||||
// Update author TextView.
|
} else {
|
||||||
manga_author.text = manga.author
|
manga.title
|
||||||
|
|
||||||
// If manga source is known update source TextView.
|
|
||||||
if (source != null) {
|
|
||||||
manga_source.text = source.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update genres TextView.
|
// Update artist TextView.
|
||||||
manga_genres.text = manga.genre
|
manga_artist.text = if (manga.artist.isNullOrBlank()) {
|
||||||
|
view.context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
manga.artist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update author TextView.
|
||||||
|
manga_author.text = if (manga.author.isNullOrBlank()) {
|
||||||
|
view.context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
manga.author
|
||||||
|
}
|
||||||
|
|
||||||
|
// If manga source is known update source TextView.
|
||||||
|
manga_source.text = if (source == null) {
|
||||||
|
view.context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
source.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update genres list
|
||||||
|
if (manga.genre.isNullOrBlank().not()) {
|
||||||
|
manga_genres_tags.setTags(manga.genre?.split(", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update description TextView.
|
||||||
|
manga_summary.text = if (manga.description.isNullOrBlank()) {
|
||||||
|
view.context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
manga.description
|
||||||
|
}
|
||||||
|
|
||||||
// Update status TextView.
|
// Update status TextView.
|
||||||
manga_status.setText(when (manga.status) {
|
manga_status.setText(when (manga.status) {
|
||||||
@ -144,9 +216,6 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
else -> R.string.unknown
|
else -> R.string.unknown
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update description TextView.
|
|
||||||
manga_summary.text = manga.description
|
|
||||||
|
|
||||||
// Set the favorite drawable to the correct one.
|
// Set the favorite drawable to the correct one.
|
||||||
setFavoriteDrawable(manga.favorite)
|
setFavoriteDrawable(manga.favorite)
|
||||||
|
|
||||||
@ -168,13 +237,26 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
manga_genres_tags.setOnTagClickListener(null)
|
||||||
|
super.onDestroyView(view)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update chapter count TextView.
|
* Update chapter count TextView.
|
||||||
*
|
*
|
||||||
* @param count number of chapters.
|
* @param count number of chapters.
|
||||||
*/
|
*/
|
||||||
fun setChapterCount(count: Float) {
|
fun setChapterCount(count: Float) {
|
||||||
manga_chapters?.text = DecimalFormat("#.#").format(count)
|
if (count > 0f) {
|
||||||
|
manga_chapters?.text = DecimalFormat("#.#").format(count)
|
||||||
|
} else {
|
||||||
|
manga_chapters?.text = resources?.getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLastUpdateDate(date: Date) {
|
||||||
|
manga_last_update?.text = DateFormat.getDateInstance(DateFormat.SHORT).format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -242,7 +324,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
fab_favorite?.setImageResource(if (isFavorite)
|
fab_favorite?.setImageResource(if (isFavorite)
|
||||||
R.drawable.ic_bookmark_white_24dp
|
R.drawable.ic_bookmark_white_24dp
|
||||||
else
|
else
|
||||||
R.drawable.ic_bookmark_border_white_24dp)
|
R.drawable.ic_add_to_library_24dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -301,6 +383,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
.showDialog(router)
|
.showDialog(router)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
|
} else {
|
||||||
|
activity?.toast(activity?.getString(R.string.manga_removed_library))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,6 +462,35 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a string to clipboard
|
||||||
|
*
|
||||||
|
* @param label Label to show to the user describing the content
|
||||||
|
* @param content the actual text to copy to the board
|
||||||
|
*/
|
||||||
|
private fun copyToClipboard(label: String, content: String) {
|
||||||
|
if (content.isBlank()) return
|
||||||
|
|
||||||
|
val activity = activity ?: return
|
||||||
|
val view = view ?: return
|
||||||
|
|
||||||
|
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
clipboard.primaryClip = ClipData.newPlainText(label, content)
|
||||||
|
|
||||||
|
activity.toast(view.context.getString(R.string.copied_to_clipboard, content.truncateCenter(20)),
|
||||||
|
Toast.LENGTH_SHORT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a global search using the provided query.
|
||||||
|
*
|
||||||
|
* @param query the search query to pass to the search controller
|
||||||
|
*/
|
||||||
|
fun performGlobalSearch(query: String) {
|
||||||
|
val router = parentController?.router ?: return
|
||||||
|
router.pushController(CatalogueSearchController(query).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create shortcut using ShortcutManager.
|
* Create shortcut using ShortcutManager.
|
||||||
*
|
*
|
||||||
|
@ -18,6 +18,7 @@ import rx.android.schedulers.AndroidSchedulers
|
|||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of MangaInfoFragment.
|
* Presenter of MangaInfoFragment.
|
||||||
@ -28,6 +29,7 @@ class MangaInfoPresenter(
|
|||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val source: Source,
|
val source: Source,
|
||||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
private val chapterCountRelay: BehaviorRelay<Float>,
|
||||||
|
private val lastUpdateRelay: BehaviorRelay<Date>,
|
||||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
@ -37,7 +39,7 @@ class MangaInfoPresenter(
|
|||||||
/**
|
/**
|
||||||
* Subscription to send the manga to the view.
|
* Subscription to send the manga to the view.
|
||||||
*/
|
*/
|
||||||
private var viewMangaSubcription: Subscription? = null
|
private var viewMangaSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription to update the manga from the source.
|
* Subscription to update the manga from the source.
|
||||||
@ -56,14 +58,18 @@ class MangaInfoPresenter(
|
|||||||
mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
|
mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { setFavorite(it) }
|
.subscribe { setFavorite(it) }
|
||||||
.apply { add(this) }
|
.apply { add(this) }
|
||||||
|
|
||||||
|
//update last update date
|
||||||
|
lastUpdateRelay.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeLatestCache(MangaInfoController::setLastUpdateDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the active manga to the view.
|
* Sends the active manga to the view.
|
||||||
*/
|
*/
|
||||||
fun sendMangaToView() {
|
fun sendMangaToView() {
|
||||||
viewMangaSubcription?.let { remove(it) }
|
viewMangaSubscription?.let { remove(it) }
|
||||||
viewMangaSubcription = Observable.just(manga)
|
viewMangaSubscription = Observable.just(manga)
|
||||||
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
|
||||||
|
class MangaAdapter(controller: MigrationController) :
|
||||||
|
FlexibleAdapter<IFlexible<*>>(null, controller) {
|
||||||
|
|
||||||
|
private var items: List<IFlexible<*>>? = null
|
||||||
|
|
||||||
|
override fun updateDataSet(items: MutableList<IFlexible<*>>?) {
|
||||||
|
if (this.items !== items) {
|
||||||
|
this.items = items
|
||||||
|
super.updateDataSet(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
|
import kotlinx.android.synthetic.main.catalogue_list_item.*
|
||||||
|
|
||||||
|
class MangaHolder(
|
||||||
|
private val view: View,
|
||||||
|
private val adapter: FlexibleAdapter<*>
|
||||||
|
) : BaseFlexibleViewHolder(view, adapter) {
|
||||||
|
|
||||||
|
fun bind(item: MangaItem) {
|
||||||
|
// Update the title of the manga.
|
||||||
|
title.text = item.manga.title
|
||||||
|
|
||||||
|
// Create thumbnail onclick to simulate long click
|
||||||
|
thumbnail.setOnClickListener {
|
||||||
|
// Simulate long click on this view to enter selection mode
|
||||||
|
onLongClick(itemView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the cover.
|
||||||
|
GlideApp.with(itemView.context).clear(thumbnail)
|
||||||
|
GlideApp.with(itemView.context)
|
||||||
|
.load(item.manga)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
|
.centerCrop()
|
||||||
|
.circleCrop()
|
||||||
|
.dontAnimate()
|
||||||
|
.into(thumbnail)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
||||||
|
class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>() {
|
||||||
|
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.catalogue_list_item
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): MangaHolder {
|
||||||
|
return MangaHolder(view, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||||
|
holder: MangaHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: List<Any?>?) {
|
||||||
|
|
||||||
|
holder.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other is MangaItem) {
|
||||||
|
return manga.id == other.manga.id
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return manga.id!!.hashCode()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import kotlinx.android.synthetic.main.migration_controller.*
|
||||||
|
|
||||||
|
class MigrationController : NucleusController<MigrationPresenter>(),
|
||||||
|
FlexibleAdapter.OnItemClickListener,
|
||||||
|
SourceAdapter.OnSelectClickListener {
|
||||||
|
|
||||||
|
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||||
|
|
||||||
|
private var title: String? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
setTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPresenter(): MigrationPresenter {
|
||||||
|
return MigrationPresenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
return inflater.inflate(R.layout.migration_controller, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
super.onViewCreated(view)
|
||||||
|
|
||||||
|
adapter = FlexibleAdapter(null, this)
|
||||||
|
migration_recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
|
migration_recycler.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
adapter = null
|
||||||
|
super.onDestroyView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String? {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleBack(): Boolean {
|
||||||
|
return if (presenter.state.selectedSource != null) {
|
||||||
|
presenter.deselectSource()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
super.handleBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render(state: ViewState) {
|
||||||
|
if (state.selectedSource == null) {
|
||||||
|
title = resources?.getString(R.string.label_migration)
|
||||||
|
if (adapter !is SourceAdapter) {
|
||||||
|
adapter = SourceAdapter(this)
|
||||||
|
migration_recycler.adapter = adapter
|
||||||
|
}
|
||||||
|
adapter?.updateDataSet(state.sourcesWithManga)
|
||||||
|
} else {
|
||||||
|
title = state.selectedSource.toString()
|
||||||
|
if (adapter !is MangaAdapter) {
|
||||||
|
adapter = MangaAdapter(this)
|
||||||
|
migration_recycler.adapter = adapter
|
||||||
|
}
|
||||||
|
adapter?.updateDataSet(state.mangaForSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renderIsReplacingManga(state: ViewState) {
|
||||||
|
if (state.isReplacingManga) {
|
||||||
|
if (router.getControllerWithTag(LOADING_DIALOG_TAG) == null) {
|
||||||
|
LoadingController().showDialog(router, LOADING_DIALOG_TAG)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
router.popControllerWithTag(LOADING_DIALOG_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(position: Int): Boolean {
|
||||||
|
val item = adapter?.getItem(position) ?: return false
|
||||||
|
|
||||||
|
if (item is MangaItem) {
|
||||||
|
val controller = SearchController(item.manga)
|
||||||
|
controller.targetController = this
|
||||||
|
|
||||||
|
router.pushController(controller.withFadeTransaction())
|
||||||
|
} else if (item is SourceItem) {
|
||||||
|
presenter.setSelectedSource(item.source)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectClick(position: Int) {
|
||||||
|
onItemClick(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun migrateManga(prevManga: Manga, manga: Manga) {
|
||||||
|
presenter.migrateManga(prevManga, manga, replace = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyManga(prevManga: Manga, manga: Manga) {
|
||||||
|
presenter.migrateManga(prevManga, manga, replace = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadingController : DialogController() {
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
return MaterialDialog.Builder(activity!!)
|
||||||
|
.progress(true, 0)
|
||||||
|
.content(R.string.migrating)
|
||||||
|
.cancelable(false)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LOADING_DIALOG_TAG = "LoadingDialog"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
object MigrationFlags {
|
||||||
|
|
||||||
|
private const val CHAPTERS = 0b001
|
||||||
|
private const val CATEGORIES = 0b010
|
||||||
|
private const val TRACK = 0b100
|
||||||
|
|
||||||
|
private const val CHAPTERS2 = 0x1
|
||||||
|
private const val CATEGORIES2 = 0x2
|
||||||
|
private const val TRACK2 = 0x4
|
||||||
|
|
||||||
|
val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track)
|
||||||
|
|
||||||
|
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK)
|
||||||
|
|
||||||
|
fun hasChapters(value: Int): Boolean {
|
||||||
|
return value and CHAPTERS != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasCategories(value: Int): Boolean {
|
||||||
|
return value and CATEGORIES != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasTracks(value: Int): Boolean {
|
||||||
|
return value and TRACK != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
||||||
|
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFlagsFromPositions(positions: Array<Int>): Int {
|
||||||
|
return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) })
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.util.combineLatest
|
||||||
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
|
import rx.Observable
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class MigrationPresenter(
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
) : BasePresenter<MigrationController>() {
|
||||||
|
|
||||||
|
var state = ViewState()
|
||||||
|
private set(value) {
|
||||||
|
field = value
|
||||||
|
stateRelay.call(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val stateRelay = BehaviorRelay.create(state)
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
db.getLibraryMangas()
|
||||||
|
.asRxObservable()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnNext { state = state.copy(sourcesWithManga = findSourcesWithManga(it)) }
|
||||||
|
.combineLatest(stateRelay.map { it.selectedSource }
|
||||||
|
.distinctUntilChanged(),
|
||||||
|
{ library, source -> library to source })
|
||||||
|
.filter { (_, source) -> source != null }
|
||||||
|
.observeOn(Schedulers.io())
|
||||||
|
.map { (library, source) -> libraryToMigrationItem(library, source!!.id) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnNext { state = state.copy(mangaForSource = it) }
|
||||||
|
.subscribe()
|
||||||
|
|
||||||
|
stateRelay
|
||||||
|
// Render the view when any field other than isReplacingManga changes
|
||||||
|
.distinctUntilChanged { t1, t2 -> t1.isReplacingManga != t2.isReplacingManga }
|
||||||
|
.subscribeLatestCache(MigrationController::render)
|
||||||
|
|
||||||
|
stateRelay.distinctUntilChanged { state -> state.isReplacingManga }
|
||||||
|
.subscribeLatestCache(MigrationController::renderIsReplacingManga)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelectedSource(source: Source) {
|
||||||
|
state = state.copy(selectedSource = source, mangaForSource = emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deselectSource() {
|
||||||
|
state = state.copy(selectedSource = null, mangaForSource = emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
|
||||||
|
val header = SelectionHeader()
|
||||||
|
return library.map { it.source }.toSet()
|
||||||
|
.mapNotNull { if (it != LocalSource.ID) sourceManager.get(it) else null }
|
||||||
|
.map { SourceItem(it, header) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun libraryToMigrationItem(library: List<Manga>, sourceId: Long): List<MangaItem> {
|
||||||
|
return library.filter { it.source == sourceId }.map(::MangaItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean) {
|
||||||
|
val source = sourceManager.get(manga.source) ?: return
|
||||||
|
|
||||||
|
state = state.copy(isReplacingManga = true)
|
||||||
|
|
||||||
|
Observable.defer { source.fetchChapterList(manga) }
|
||||||
|
.onErrorReturn { emptyList() }
|
||||||
|
.doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) }
|
||||||
|
.onErrorReturn { emptyList() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnUnsubscribe { state = state.copy(isReplacingManga = false) }
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateMangaInternal(source: Source, sourceChapters: List<SChapter>,
|
||||||
|
prevManga: Manga, manga: Manga, replace: Boolean) {
|
||||||
|
|
||||||
|
val flags = preferences.migrateFlags().getOrDefault()
|
||||||
|
val migrateChapters = MigrationFlags.hasChapters(flags)
|
||||||
|
val migrateCategories = MigrationFlags.hasCategories(flags)
|
||||||
|
val migrateTracks = MigrationFlags.hasTracks(flags)
|
||||||
|
|
||||||
|
db.inTransaction {
|
||||||
|
// Update chapters read
|
||||||
|
if (migrateChapters) {
|
||||||
|
try {
|
||||||
|
syncChaptersWithSource(db, sourceChapters, manga, source)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Worst case, chapters won't be synced
|
||||||
|
}
|
||||||
|
|
||||||
|
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
|
||||||
|
val maxChapterRead = prevMangaChapters.filter { it.read }
|
||||||
|
.maxBy { it.chapter_number }?.chapter_number
|
||||||
|
if (maxChapterRead != null) {
|
||||||
|
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
|
for (chapter in dbChapters) {
|
||||||
|
if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) {
|
||||||
|
chapter.read = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.insertChapters(dbChapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update categories
|
||||||
|
if (migrateCategories) {
|
||||||
|
val categories = db.getCategoriesForManga(prevManga).executeAsBlocking()
|
||||||
|
val mangaCategories = categories.map { MangaCategory.create(manga, it) }
|
||||||
|
db.setMangaCategories(mangaCategories, listOf(manga))
|
||||||
|
}
|
||||||
|
// Update track
|
||||||
|
if (migrateTracks) {
|
||||||
|
val tracks = db.getTracks(prevManga).executeAsBlocking()
|
||||||
|
for (track in tracks) {
|
||||||
|
track.id = null
|
||||||
|
track.manga_id = manga.id!!
|
||||||
|
}
|
||||||
|
db.insertTracks(tracks).executeAsBlocking()
|
||||||
|
}
|
||||||
|
// Update favorite status
|
||||||
|
if (replace) {
|
||||||
|
prevManga.favorite = false
|
||||||
|
db.updateMangaFavorite(prevManga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
manga.favorite = true
|
||||||
|
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class SearchController(
|
||||||
|
private var manga: Manga? = null
|
||||||
|
) : CatalogueSearchController(manga?.title) {
|
||||||
|
|
||||||
|
private var newManga: Manga? = null
|
||||||
|
|
||||||
|
override fun createPresenter(): CatalogueSearchPresenter {
|
||||||
|
return SearchPresenter(initialQuery, manga!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putSerializable(::manga.name, manga)
|
||||||
|
outState.putSerializable(::newManga.name, newManga)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
manga = savedInstanceState.getSerializable(::manga.name) as? Manga
|
||||||
|
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
|
||||||
|
}
|
||||||
|
|
||||||
|
fun migrateManga() {
|
||||||
|
val target = targetController as? MigrationController ?: return
|
||||||
|
val manga = manga ?: return
|
||||||
|
val newManga = newManga ?: return
|
||||||
|
|
||||||
|
router.popController(this)
|
||||||
|
target.migrateManga(manga, newManga)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyManga() {
|
||||||
|
val target = targetController as? MigrationController ?: return
|
||||||
|
val manga = manga ?: return
|
||||||
|
val newManga = newManga ?: return
|
||||||
|
|
||||||
|
router.popController(this)
|
||||||
|
target.copyManga(manga, newManga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMangaClick(manga: Manga) {
|
||||||
|
newManga = manga
|
||||||
|
val dialog = MigrationDialog()
|
||||||
|
dialog.targetController = this
|
||||||
|
dialog.showDialog(router)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMangaLongClick(manga: Manga) {
|
||||||
|
// Call parent's default click listener
|
||||||
|
super.onMangaClick(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
class MigrationDialog : DialogController() {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
val prefValue = preferences.migrateFlags().getOrDefault()
|
||||||
|
|
||||||
|
val preselected = MigrationFlags.getEnabledFlagsPositions(prefValue)
|
||||||
|
|
||||||
|
return MaterialDialog.Builder(activity!!)
|
||||||
|
.content(R.string.migration_dialog_what_to_include)
|
||||||
|
.items(MigrationFlags.titles.map { resources?.getString(it) })
|
||||||
|
.alwaysCallMultiChoiceCallback()
|
||||||
|
.itemsCallbackMultiChoice(preselected.toTypedArray(), { _, positions, _ ->
|
||||||
|
// Save current settings for the next time
|
||||||
|
val newValue = MigrationFlags.getFlagsFromPositions(positions)
|
||||||
|
preferences.migrateFlags().set(newValue)
|
||||||
|
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.positiveText(R.string.migrate)
|
||||||
|
.negativeText(R.string.copy)
|
||||||
|
.neutralText(android.R.string.cancel)
|
||||||
|
.onPositive { _, _ ->
|
||||||
|
(targetController as? SearchController)?.migrateManga()
|
||||||
|
}
|
||||||
|
.onNegative { _, _ ->
|
||||||
|
(targetController as? SearchController)?.copyManga()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
|
||||||
|
|
||||||
|
class SearchPresenter(
|
||||||
|
initialQuery: String? = "",
|
||||||
|
private val manga: Manga
|
||||||
|
) : CatalogueSearchPresenter(initialQuery) {
|
||||||
|
|
||||||
|
override fun getEnabledSources(): List<CatalogueSource> {
|
||||||
|
// Filter out the source of the selected manga
|
||||||
|
return super.getEnabledSources()
|
||||||
|
.filterNot { it.id == manga.source }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
|
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item that contains the selection header.
|
||||||
|
*/
|
||||||
|
class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the layout resource of this item.
|
||||||
|
*/
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.catalogue_main_controller_card
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new view holder for this item.
|
||||||
|
*/
|
||||||
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
|
||||||
|
return SelectionHeader.Holder(view, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds this item to the given view holder.
|
||||||
|
*/
|
||||||
|
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder,
|
||||||
|
position: Int, payloads: List<Any?>?) {
|
||||||
|
// Intentionally empty
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder(view: View, adapter: FlexibleAdapter<*>) : BaseFlexibleViewHolder(view, adapter) {
|
||||||
|
init {
|
||||||
|
title.text = "Please select a source to migrate from"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return other is SelectionHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.getResourceColor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter that holds the catalogue cards.
|
||||||
|
*
|
||||||
|
* @param controller instance of [MigrationController].
|
||||||
|
*/
|
||||||
|
class SourceAdapter(val controller: MigrationController) :
|
||||||
|
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||||
|
|
||||||
|
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
|
||||||
|
|
||||||
|
private var items: List<IFlexible<*>>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
setDisplayHeadersAtStartUp(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for browse item clicks.
|
||||||
|
*/
|
||||||
|
val selectClickListener: OnSelectClickListener? = controller
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener which should be called when user clicks select.
|
||||||
|
*/
|
||||||
|
interface OnSelectClickListener {
|
||||||
|
fun onSelectClick(position: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateDataSet(items: MutableList<IFlexible<*>>?) {
|
||||||
|
if (this.items !== items) {
|
||||||
|
this.items = items
|
||||||
|
super.updateDataSet(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
|
import eu.kanade.tachiyomi.util.getRound
|
||||||
|
import eu.kanade.tachiyomi.util.gone
|
||||||
|
import io.github.mthli.slice.Slice
|
||||||
|
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
|
||||||
|
|
||||||
|
class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
||||||
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
|
SlicedHolder {
|
||||||
|
|
||||||
|
override val slice = Slice(card).apply {
|
||||||
|
setColor(adapter.cardBackground)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val viewToSlice: View
|
||||||
|
get() = card
|
||||||
|
|
||||||
|
init {
|
||||||
|
source_latest.gone()
|
||||||
|
source_browse.setText(R.string.select)
|
||||||
|
source_browse.setOnClickListener {
|
||||||
|
adapter.selectClickListener?.onSelectClick(adapterPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(item: SourceItem) {
|
||||||
|
val source = item.source
|
||||||
|
setCardEdges(item)
|
||||||
|
|
||||||
|
// Set source name
|
||||||
|
title.text = source.name
|
||||||
|
|
||||||
|
// Set circle letter image.
|
||||||
|
itemView.post {
|
||||||
|
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item that contains source information.
|
||||||
|
*
|
||||||
|
* @param source Instance of [Source] containing source information.
|
||||||
|
* @param header The header for this item.
|
||||||
|
*/
|
||||||
|
data class SourceItem(val source: Source, val header: SelectionHeader? = null) :
|
||||||
|
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the layout resource of this item.
|
||||||
|
*/
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.catalogue_main_controller_card_item
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new view holder for this item.
|
||||||
|
*/
|
||||||
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder {
|
||||||
|
return SourceHolder(view, adapter as SourceAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds this item to the given view holder.
|
||||||
|
*/
|
||||||
|
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder,
|
||||||
|
position: Int, payloads: List<Any?>?) {
|
||||||
|
|
||||||
|
holder.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
|
data class ViewState(
|
||||||
|
val selectedSource: Source? = null,
|
||||||
|
val mangaForSource: List<MangaItem> = emptyList(),
|
||||||
|
val sourcesWithManga: List<SourceItem> = emptyList(),
|
||||||
|
val isReplacingManga: Boolean = false
|
||||||
|
)
|
@ -62,6 +62,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
with(image_view) {
|
with(image_view) {
|
||||||
setMaxTileSize((reader.activity as ReaderActivity).maxBitmapSize)
|
setMaxTileSize((reader.activity as ReaderActivity).maxBitmapSize)
|
||||||
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
|
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
|
||||||
|
setDoubleTapZoomDuration(reader.doubleTapAnimDuration.toInt())
|
||||||
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
||||||
setMinimumScaleType(reader.scaleType)
|
setMinimumScaleType(reader.scaleType)
|
||||||
setMinimumDpi(90)
|
setMinimumDpi(90)
|
||||||
|
@ -85,6 +85,12 @@ abstract class PagerReader : BaseReader() {
|
|||||||
var cropBorders: Boolean = false
|
var cropBorders: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of the double tap animation
|
||||||
|
*/
|
||||||
|
var doubleTapAnimDuration = 500
|
||||||
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scale type (fit width, fit screen, etc).
|
* Scale type (fit width, fit screen, etc).
|
||||||
*/
|
*/
|
||||||
@ -166,6 +172,10 @@ abstract class PagerReader : BaseReader() {
|
|||||||
.skip(1)
|
.skip(1)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.subscribe { refreshAdapter() })
|
.subscribe { refreshAdapter() })
|
||||||
|
|
||||||
|
add(preferences.doubleTapAnimSpeed()
|
||||||
|
.asObservable()
|
||||||
|
.subscribe { doubleTapAnimDuration = it })
|
||||||
}
|
}
|
||||||
|
|
||||||
setPagesOnAdapter()
|
setPagesOnAdapter()
|
||||||
|
@ -57,6 +57,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
|
|||||||
with(image_view) {
|
with(image_view) {
|
||||||
setMaxTileSize(readerActivity.maxBitmapSize)
|
setMaxTileSize(readerActivity.maxBitmapSize)
|
||||||
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
|
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
|
||||||
|
setDoubleTapZoomDuration(webtoonReader.doubleTapAnimDuration.toInt())
|
||||||
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
||||||
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
|
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
|
||||||
setMinimumDpi(90)
|
setMinimumDpi(90)
|
||||||
|
@ -59,6 +59,12 @@ class WebtoonReader : BaseReader() {
|
|||||||
var cropBorders: Boolean = false
|
var cropBorders: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of the double tap animation
|
||||||
|
*/
|
||||||
|
var doubleTapAnimDuration = 500
|
||||||
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gesture detector for image touch events.
|
* Gesture detector for image touch events.
|
||||||
*/
|
*/
|
||||||
@ -124,6 +130,10 @@ class WebtoonReader : BaseReader() {
|
|||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.subscribe { refreshAdapter() })
|
.subscribe { refreshAdapter() })
|
||||||
|
|
||||||
|
subscriptions.add(readerActivity.preferences.doubleTapAnimSpeed()
|
||||||
|
.asObservable()
|
||||||
|
.subscribe { doubleTapAnimDuration = it })
|
||||||
|
|
||||||
setPagesOnAdapter()
|
setPagesOnAdapter()
|
||||||
return recycler
|
return recycler
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat
|
import android.support.graphics.drawable.VectorDrawableCompat
|
||||||
import android.support.v4.graphics.drawable.DrawableCompat
|
import android.support.v4.graphics.drawable.DrawableCompat
|
||||||
import android.support.v7.preference.*
|
import android.support.v7.preference.*
|
||||||
@ -10,7 +9,7 @@ import eu.kanade.tachiyomi.widget.preference.IntListPreference
|
|||||||
@Target(AnnotationTarget.TYPE)
|
@Target(AnnotationTarget.TYPE)
|
||||||
annotation class DSL
|
annotation class DSL
|
||||||
|
|
||||||
inline fun PreferenceManager.newScreen(context: Context, block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen {
|
inline fun PreferenceManager.newScreen(block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen {
|
||||||
return createPreferenceScreen(context).also { it.block() }
|
return createPreferenceScreen(context).also { it.block() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,8 +9,8 @@ import eu.kanade.tachiyomi.BuildConfig
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker
|
import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker
|
||||||
import eu.kanade.tachiyomi.data.updater.GithubUpdateResult
|
import eu.kanade.tachiyomi.data.updater.GithubUpdateResult
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateDownloaderService
|
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -51,9 +51,9 @@ class SettingsAboutController : SettingsController() {
|
|||||||
onChange { newValue ->
|
onChange { newValue ->
|
||||||
val checked = newValue as Boolean
|
val checked = newValue as Boolean
|
||||||
if (checked) {
|
if (checked) {
|
||||||
UpdateCheckerJob.setupTask()
|
UpdaterJob.setupTask()
|
||||||
} else {
|
} else {
|
||||||
UpdateCheckerJob.cancelTask()
|
UpdaterJob.cancelTask()
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -131,7 +131,7 @@ class SettingsAboutController : SettingsController() {
|
|||||||
if (appContext != null) {
|
if (appContext != null) {
|
||||||
// Start download
|
// Start download
|
||||||
val url = args.getString(URL_KEY)
|
val url = args.getString(URL_KEY)
|
||||||
UpdateDownloaderService.downloadUpdate(appContext, url)
|
UpdaterService.downloadUpdate(appContext, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting
|
|||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@ -26,10 +27,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
import eu.kanade.tachiyomi.util.getUriCompat
|
import eu.kanade.tachiyomi.util.*
|
||||||
import eu.kanade.tachiyomi.util.registerLocalReceiver
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import eu.kanade.tachiyomi.util.unregisterLocalReceiver
|
|
||||||
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -116,19 +114,22 @@ class SettingsBackupController : SettingsController() {
|
|||||||
|
|
||||||
onClick {
|
onClick {
|
||||||
val currentDir = preferences.backupsDirectory().getOrDefault()
|
val currentDir = preferences.backupsDirectory().getOrDefault()
|
||||||
|
try{
|
||||||
val intent = if (Build.VERSION.SDK_INT < 21) {
|
val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// Custom dir selected, open directory selector
|
// Custom dir selected, open directory selector
|
||||||
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
|
preferences.context.getFilePicker(currentDir)
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
} else {
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
}
|
||||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
|
||||||
|
|
||||||
} else {
|
startActivityForResult(intent, CODE_BACKUP_DIR)
|
||||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
} catch (e: ActivityNotFoundException){
|
||||||
|
//Fall back to custom picker on error
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
|
||||||
|
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
startActivityForResult(intent, CODE_BACKUP_DIR)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences.backupsDirectory().asObservable()
|
preferences.backupsDirectory().asObservable()
|
||||||
@ -204,25 +205,30 @@ class SettingsBackupController : SettingsController() {
|
|||||||
fun createBackup(flags: Int) {
|
fun createBackup(flags: Int) {
|
||||||
backupFlags = flags
|
backupFlags = flags
|
||||||
|
|
||||||
// If API lower as KitKat use custom dir picker
|
// Setup custom file picker intent
|
||||||
val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
// Get dirs
|
||||||
// Get dirs
|
val currentDir = preferences.backupsDirectory().getOrDefault()
|
||||||
val preferences: PreferencesHelper = Injekt.get()
|
|
||||||
val currentDir = preferences.backupsDirectory().getOrDefault()
|
|
||||||
|
|
||||||
Intent(activity, CustomLayoutPickerActivity::class.java)
|
try {
|
||||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
// If API is lower than Lollipop use custom picker
|
||||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
preferences.context.getFilePicker(currentDir)
|
||||||
.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
} else {
|
||||||
} else {
|
// Use Androids build in file creator
|
||||||
// Use Androids build in file creator
|
Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||||
Intent(Intent.ACTION_CREATE_DOCUMENT)
|
|
||||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
.setType("application/*")
|
.setType("application/*")
|
||||||
.putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename())
|
.putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename())
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivityForResult(intent, CODE_BACKUP_CREATE)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
// Handle errors where the android ROM doesn't support the built in picker
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
|
||||||
|
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_CREATE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
startActivityForResult(intent, CODE_BACKUP_CREATE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateBackupDialog : DialogController() {
|
class CreateBackupDialog : DialogController() {
|
||||||
|
@ -10,8 +10,11 @@ import android.view.ContextThemeWrapper
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
@ -55,9 +58,23 @@ abstract class SettingsController : PreferenceController() {
|
|||||||
return preferenceScreen?.title?.toString()
|
return preferenceScreen?.title?.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(view: View) {
|
fun setTitle() {
|
||||||
|
var parentController = parentController
|
||||||
|
while (parentController != null) {
|
||||||
|
if (parentController is BaseController && parentController.getTitle() != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parentController = parentController.parentController
|
||||||
|
}
|
||||||
|
|
||||||
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
|
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
|
||||||
super.onAttach(view)
|
}
|
||||||
|
|
||||||
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
|
if (type.isEnter) {
|
||||||
|
setTitle()
|
||||||
|
}
|
||||||
|
super.onChangeStarted(handler, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
|
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.setting
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@ -18,6 +19,7 @@ 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.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.getFilePicker
|
||||||
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -150,17 +152,19 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun customDirectorySelected(currentDir: String) {
|
fun customDirectorySelected(currentDir: String) {
|
||||||
if (Build.VERSION.SDK_INT < 21) {
|
|
||||||
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
|
||||||
|
|
||||||
startActivityForResult(i, DOWNLOAD_DIR_PRE_L)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR_PRE_L)
|
||||||
} else {
|
} else {
|
||||||
val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
startActivityForResult(i, DOWNLOAD_DIR_L)
|
try {
|
||||||
|
startActivityForResult(intent, DOWNLOAD_DIR_L)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR_L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +62,14 @@ class SettingsReaderController : SettingsController() {
|
|||||||
defaultValue = "0"
|
defaultValue = "0"
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
}
|
}
|
||||||
|
intListPreference {
|
||||||
|
key = Keys.doubleTapAnimationSpeed
|
||||||
|
titleRes = R.string.pref_double_tap_anim_speed
|
||||||
|
entries = arrayOf(context.getString(R.string.double_tap_anim_speed_0), context.getString(R.string.double_tap_anim_speed_fast), context.getString(R.string.double_tap_anim_speed_normal))
|
||||||
|
entryValues = arrayOf("1", "250", "500") // using a value of 0 breaks the image viewer, so min is 1
|
||||||
|
defaultValue = "500"
|
||||||
|
summary = "%s"
|
||||||
|
}
|
||||||
switchPreference {
|
switchPreference {
|
||||||
key = Keys.fullscreen
|
key = Keys.fullscreen
|
||||||
titleRes = R.string.pref_fullscreen
|
titleRes = R.string.pref_fullscreen
|
||||||
|
@ -11,7 +11,7 @@ object ChapterRecognition {
|
|||||||
* All cases with Ch.xx
|
* All cases with Ch.xx
|
||||||
* Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation -R> 4
|
* Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation -R> 4
|
||||||
*/
|
*/
|
||||||
private val basic = Regex("""(?<=ch\.)([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""")
|
private val basic = Regex("""(?<=ch\.) *([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regex used when only one number occurrence
|
* Regex used when only one number occurrence
|
||||||
|
@ -16,6 +16,8 @@ import android.support.v4.app.NotificationCompat
|
|||||||
import android.support.v4.content.ContextCompat
|
import android.support.v4.content.ContextCompat
|
||||||
import android.support.v4.content.LocalBroadcastManager
|
import android.support.v4.content.LocalBroadcastManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||||
|
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a toast in this context.
|
* Display a toast in this context.
|
||||||
@ -50,6 +52,19 @@ inline fun Context.notification(channelId: String, func: NotificationCompat.Buil
|
|||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to construct an Intent to use a custom file picker.
|
||||||
|
* @param currentDir the path the file picker will open with.
|
||||||
|
* @return an Intent to start the file picker activity.
|
||||||
|
*/
|
||||||
|
fun Context.getFilePicker(currentDir: String): Intent {
|
||||||
|
return Intent(this, CustomLayoutPickerActivity::class.java)
|
||||||
|
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||||
|
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||||
|
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
||||||
|
.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the give permission is granted.
|
* Checks if the give permission is granted.
|
||||||
*
|
*
|
||||||
|
@ -13,9 +13,8 @@ import java.io.File
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
*/
|
*/
|
||||||
fun File.getUriCompat(context: Context): Uri {
|
fun File.getUriCompat(context: Context): Uri {
|
||||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||||
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this)
|
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this)
|
||||||
else Uri.fromFile(this)
|
else Uri.fromFile(this)
|
||||||
return uri
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.util
|
package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
|
import java.lang.Math.floor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the given string to have at most [count] characters using [replacement] at its end.
|
* Replaces the given string to have at most [count] characters using [replacement] at its end.
|
||||||
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
|
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
|
||||||
@ -11,3 +13,16 @@ fun String.chop(count: Int, replacement: String = "..."): String {
|
|||||||
this
|
this
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the given string to have at most [count] characters using [replacement] near the center.
|
||||||
|
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
|
||||||
|
*/
|
||||||
|
fun String.truncateCenter(count: Int, replacement: String = "..."): String{
|
||||||
|
if(length <= count)
|
||||||
|
return this
|
||||||
|
|
||||||
|
val pieceLength:Int = floor((count - replacement.length).div(2.0)).toInt()
|
||||||
|
|
||||||
|
return "${ take(pieceLength) }$replacement${ takeLast(pieceLength) }"
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
package eu.kanade.tachiyomi.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import kotlinx.android.synthetic.main.download_custom_amount.view.*
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom dialog to select how many chapters to download.
|
||||||
|
*/
|
||||||
|
class DialogCustomDownloadView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||||
|
LinearLayout(context, attrs) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current amount of custom download chooser.
|
||||||
|
*/
|
||||||
|
var amount: Int = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal value of custom download chooser.
|
||||||
|
*/
|
||||||
|
private var min = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximal value of custom download chooser.
|
||||||
|
*/
|
||||||
|
private var max = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Add view to stack
|
||||||
|
addView(inflate(R.layout.download_custom_amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when view is added
|
||||||
|
*
|
||||||
|
* @param child
|
||||||
|
*/
|
||||||
|
override fun onViewAdded(child: View) {
|
||||||
|
super.onViewAdded(child)
|
||||||
|
|
||||||
|
// Set download count to 0.
|
||||||
|
myNumber.text = SpannableStringBuilder(getAmount(0).toString())
|
||||||
|
|
||||||
|
// When user presses button decrease amount by 10.
|
||||||
|
btn_decrease_10.setOnClickListener {
|
||||||
|
myNumber.text = SpannableStringBuilder(getAmount(amount - 10).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// When user presses button increase amount by 10.
|
||||||
|
btn_increase_10.setOnClickListener {
|
||||||
|
myNumber.text = SpannableStringBuilder(getAmount(amount + 10).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// When user presses button decrease amount by 1.
|
||||||
|
btn_decrease.setOnClickListener {
|
||||||
|
myNumber.text = SpannableStringBuilder(getAmount(amount - 1).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// When user presses button increase amount by 1.
|
||||||
|
btn_increase.setOnClickListener {
|
||||||
|
myNumber.text = SpannableStringBuilder(getAmount(amount + 1).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// When user inputs custom number set amount equal to input.
|
||||||
|
myNumber.addTextChangedListener(object : SimpleTextWatcher() {
|
||||||
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||||
|
try {
|
||||||
|
amount = getAmount(Integer.parseInt(s.toString()))
|
||||||
|
} catch (error: NumberFormatException) {
|
||||||
|
// Catch NumberFormatException to prevent parse exception when input is empty.
|
||||||
|
Timber.e(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set min max of custom download amount chooser.
|
||||||
|
* @param min minimal downloads
|
||||||
|
* @param max maximal downloads
|
||||||
|
*/
|
||||||
|
fun setMinMax(min: Int, max: Int) {
|
||||||
|
this.min = min
|
||||||
|
this.max = max
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns amount to download.
|
||||||
|
* if minimal downloads is less than input return minimal downloads.
|
||||||
|
* if Maximal downloads is more than input return maximal downloads.
|
||||||
|
*
|
||||||
|
* @return amount to download.
|
||||||
|
*/
|
||||||
|
private fun getAmount(input: Int): Int {
|
||||||
|
return when {
|
||||||
|
input > max -> max
|
||||||
|
input < min -> min
|
||||||
|
else -> input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
app/src/main/res/drawable/ic_add_to_library_24dp.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M17,18V5H7V18L12,15.82L17,18M17,3A2,2 0 0,1 19,5V21L12,18L5,21V5C5,3.89 5.9,3 7,3H17M11,7H13V9H15V11H13V13H11V11H9V9H11V7Z" />
|
||||||
|
</vector>
|
16
app/src/main/res/drawable/ic_arrow_down_32dp.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="32dp"
|
||||||
|
android:height="32dp"
|
||||||
|
android:viewportHeight="32"
|
||||||
|
android:viewportWidth="32">
|
||||||
|
<group
|
||||||
|
android:scaleX="0.8"
|
||||||
|
android:scaleY="0.8"
|
||||||
|
android:pivotX="32"
|
||||||
|
android:pivotY="32"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M11,4H13V16L18.5,10.5L19.92,11.92L12,19.84L4.08,11.92L5.5,10.5L11,16V4Z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
16
app/src/main/res/drawable/ic_arrow_up_32dp.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="32dp"
|
||||||
|
android:height="32dp"
|
||||||
|
android:viewportHeight="32"
|
||||||
|
android:viewportWidth="32">
|
||||||
|
<group
|
||||||
|
android:scaleX="0.8"
|
||||||
|
android:scaleY="0.8"
|
||||||
|
android:pivotX="32"
|
||||||
|
android:pivotY="32"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/ic_chevron_left_black_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z" />
|
||||||
|
</vector>
|
@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:pathData="M11.9,16.6l-4.6,-4.6l4.6,-4.6l-1.4,-1.4l-6,6l6,6z"
|
||||||
|
android:fillColor="#FF000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M18.9,16.6l-4.6,-4.6l4.6,-4.6l-1.4,-1.4l-6,6l6,6z"
|
||||||
|
android:fillColor="#FF000000" />
|
||||||
|
</vector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
|
||||||
|
</vector>
|
@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.1,16.6l4.6,-4.6l-4.6,-4.6l1.4,-1.4l6,6l-6,6z"
|
||||||
|
android:fillColor="#FF000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M5.1,16.6l4.6,-4.6l-4.6,-4.6l1.4,-1.4l6,6l-6,6z"
|
||||||
|
android:fillColor="#FF000000" />
|
||||||
|
</vector>
|
7
app/src/main/res/drawable/ic_in_library_24dp.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M12,8A3,3 0 0,0 15,5A3,3 0 0,0 12,2A3,3 0 0,0 9,5A3,3 0 0,0 12,8M12,11.54C9.64,9.35 6.5,8 3,8V19C6.5,19 9.64,20.35 12,22.54C14.36,20.35 17.5,19 21,19V8C17.5,8 14.36,9.35 12,11.54Z" />
|
||||||
|
</vector>
|
34
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108.0"
|
||||||
|
android:viewportHeight="108.0">
|
||||||
|
<path
|
||||||
|
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#2E84BF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#69A3CB"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#CE2828"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#FFF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#000"/>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/ic_more_vert_black_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:viewportWidth="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"/>
|
||||||
|
</vector>
|