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
This commit is contained in:
NerdNumber9 2018-01-29 12:16:32 -05:00
commit 8c8f2585aa
134 changed files with 2903 additions and 1215 deletions

View File

@ -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"

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -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
} }

View File

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

View File

@ -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()

View File

@ -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)
}
}

View File

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

View File

@ -11,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"

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
} }

View File

@ -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
} }

View File

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

View File

@ -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)

View File

@ -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"

View File

@ -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
}

View File

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

View File

@ -1,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())
} }
/** /**

View File

@ -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)
}
} }

View File

@ -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
}
} }

View File

@ -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))
} }
} }

View File

@ -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() }
} }

View File

@ -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
} }
} }
} }

View File

@ -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 {

View File

@ -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)
} }

View File

@ -22,6 +22,7 @@ class CatalogueSearchCardAdapter(controller: CatalogueSearchController) :
*/ */
interface OnMangaClickListener { interface OnMangaClickListener {
fun onMangaClick(manga: Manga) fun onMangaClick(manga: Manga)
fun onMangaLongClick(manga: Manga)
} }
} }

View File

@ -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)
} }

View File

@ -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.
* *

View File

@ -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()

View File

@ -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
} }

View File

@ -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 }
} }
} }

View File

@ -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)

View File

@ -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)
}
}
} }

View File

@ -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) })
} }

View File

@ -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, _ ->

View File

@ -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"
}
}

View File

@ -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.
* *

View File

@ -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) })
} }

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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"
}
}

View File

@ -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) })
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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 }
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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))
}
}
}

View File

@ -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)
}
}

View File

@ -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
)

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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
} }

View File

@ -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() }
} }

View File

@ -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()

View File

@ -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() {

View File

@ -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 {

View File

@ -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)
}
}
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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.
* *

View File

@ -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
} }

View File

@ -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) }"
}

View File

@ -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
}
}
}

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
</vector>

View File

@ -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>

View 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>

View 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>

View 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>

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